/**
 * Non-side-effect class export for `<list-ui>` (+ co-located primitives).
 *
 * Importing this file gives you the class(es) without auto-registering the tag.
 * Useful for test isolation, subclassing with tag-name override, or selective
 * composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/list`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <list-ui divider>
 *   <list-item-ui icon="check" text="Item one"></list-item-ui>
 *   <list-item-ui icon="check" text="Item two"></list-item-ui>
 * </list-ui>
 *
 * Styled list with optional icons and dividers.
 *
 * Selection (mirrors table-ui):
 *   <list-ui selectable selected-key="row-2">
 *     <list-item-ui id="row-1" text="One"></list-item-ui>
 *     <list-item-ui id="row-2" text="Two"></list-item-ui>
 *   </list-ui>
 *
 * When `selectable`:
 *   - Children (role="listitem") become keyboard-focusable via roving tabindex.
 *   - Click selects the child; key is its id or data-key.
 *   - aria-selected is set on all children.
 *   - Fires `selection-change` with { key, previousKey }.
 */

import { UIElement } from '../../core/element.js';

export class UIList extends UIElement {
  static properties = {
    divider:     { type: Boolean, default: false, reflect: true },
    spacing:     { type: String,  default: 'md',  reflect: true },
    selectable:  { type: Boolean, default: false, reflect: true },
    selectedKey: { type: String,  default: '',    reflect: true, attribute: 'selected-key' },
  };

  static template = () => null;

  #bound = false;

  connected() {
    this.setAttribute('role', 'list');
    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('click', this.#onClick);
      this.addEventListener('keydown', this.#onKeydown);
    }
  }

  disconnected() {
    this.removeEventListener('click', this.#onClick);
    this.removeEventListener('keydown', this.#onKeydown);
    this.#bound = false;
  }

  render() {
    if (!this.selectable) {
      // Clear selection chrome from children
      for (const ch of this.#items()) {
        ch.removeAttribute('aria-selected');
        ch.removeAttribute('tabindex');
      }
      return;
    }
    const key = this.selectedKey || '';
    const items = this.#items();
    let focusTarget = null;
    for (const ch of items) {
      const k = this.#keyOf(ch);
      const selected = k && k === key;
      ch.setAttribute('aria-selected', selected ? 'true' : 'false');
      ch.setAttribute('tabindex', selected ? '0' : '-1');
      if (selected) focusTarget = ch;
    }
    // Ensure at least one item is focusable (first) when nothing selected.
    if (!focusTarget && items.length) {
      items[0].setAttribute('tabindex', '0');
    }
  }

  #items() {
    // Match by TAG as well as role: a child <list-item-ui> sets role="listitem"
    // in its OWN connected(), which (light-DOM upgrade order) has NOT run yet
    // when the parent list first render()s. A role-only filter therefore found
    // zero items on the initial selectable render → aria-selected was never
    // stamped → the [aria-selected="true"] selection CSS never matched. The tag
    // name is present pre-upgrade, so this finds the rows regardless of timing;
    // the attribute (aria-selected) we set persists through their later upgrade.
    return [...this.children].filter(
      (el) => el.tagName === 'LIST-ITEM-UI' || el.getAttribute?.('role') === 'listitem',
    );
  }

  #keyOf(el) {
    return el.id || el.getAttribute('data-key') || '';
  }

  #selectKey(newKey, { focus = false } = {}) {
    if (!this.selectable) return;
    if (!newKey) return;
    const previousKey = this.selectedKey || '';
    if (newKey === previousKey) return;
    this.selectedKey = newKey;
    this.dispatchEvent(new CustomEvent('selection-change', {
      detail: { key: newKey, previousKey },
      bubbles: true,
    }));
    if (focus) {
      const el = [...this.#items()].find((i) => this.#keyOf(i) === newKey);
      el?.focus?.();
    }
  }

  #onClick = (e) => {
    if (!this.selectable) return;
    const item = e.target.closest?.('[role="listitem"]');
    if (!item || item.parentElement !== this) return;
    const key = this.#keyOf(item);
    if (key) this.#selectKey(key);
  };

  #onKeydown = (e) => {
    if (!this.selectable) return;
    const items = this.#items();
    if (!items.length) return;
    const current = document.activeElement;
    const idx = items.indexOf(current);
    if (idx < 0) return;
    let next = idx;
    switch (e.key) {
      case 'ArrowDown': next = Math.min(items.length - 1, idx + 1); break;
      case 'ArrowUp':   next = Math.max(0, idx - 1); break;
      case 'Home':      next = 0; break;
      case 'End':       next = items.length - 1; break;
      case 'Enter':
      case ' ': {
        e.preventDefault();
        const key = this.#keyOf(items[idx]);
        if (key) this.#selectKey(key);
        return;
      }
      default: return;
    }
    if (next !== idx) {
      e.preventDefault();
      items[next]?.focus?.();
    }
  };
}

export class UIListItem extends UIElement {
  static properties = {
    icon:        { type: String, default: '', reflect: true },
    text:        { type: String, default: '', reflect: true },
    description: { type: String, default: '', reflect: true },
  };

  static template = () => null;

  connected() {
    this.setAttribute('role', 'listitem');
    this.#stamp();
  }

  // Scoped to direct children — never reach into nested custom elements,
  // otherwise we'd accidentally remove e.g. a <span slot="icon"> inside a
  // child <card-ui> when list-item-ui has no `icon` prop.
  #ownChild(selector) {
    // wrapper-trap-ok: finds component-stamped children (identified by [data-list-stamped]) — consumer list-item-ui children are never queried here
    for (const ch of this.children) {
      if (ch.matches(selector)) return ch;
    }
    return null;
  }

  // Mark slot elements we create so render() never deletes consumer-provided ones.
  #stampMark(el) { el.dataset.listStamped = '1'; return el; }
  #wasStamped(el) { return el?.dataset?.listStamped === '1'; }

  #stamp() {
    if (this.#ownChild('[slot="content"]')) return;

    if (this.icon) {
      let iconEl = this.#ownChild('[slot="icon"]') || this.#ownChild('icon-ui');
      if (!iconEl) {
        iconEl = this.#stampMark(document.createElement('icon-ui'));
        iconEl.setAttribute('slot', 'icon');
        this.prepend(iconEl);
      }
      iconEl.setAttribute('name', this.icon);
    }

    if (this.text) {
      let span = this.#ownChild('[slot="text"]');
      if (!span) {
        span = this.#stampMark(document.createElement('span'));
        span.setAttribute('slot', 'text');
        this.appendChild(span);
      }
      if (this.#wasStamped(span)) span.textContent = this.text;
    }

    if (this.description) {
      let desc = this.#ownChild('[slot="description"]');
      if (!desc) {
        desc = this.#stampMark(document.createElement('span'));
        desc.setAttribute('slot', 'description');
        this.appendChild(desc);
      }
      if (this.#wasStamped(desc)) desc.textContent = this.description;
    }
  }

  render() {
    // Sync icon — only touch elements we stamped.
    const iconEl = this.#ownChild('[slot="icon"]');
    if (this.icon) {
      if (iconEl) {
        if (this.#wasStamped(iconEl)) iconEl.setAttribute('name', this.icon);
      } else {
        const el = this.#stampMark(document.createElement('icon-ui'));
        el.setAttribute('slot', 'icon');
        el.setAttribute('name', this.icon);
        this.prepend(el);
      }
    } else if (this.#wasStamped(iconEl)) {
      iconEl.remove();
    }

    // Sync text — mirror the icon + description branches: create-if-missing
    // path so post-connect property bindings (template engine `.text=${expr}`)
    // can stamp the span when `#stamp()` ran with `this.text === ''`. §340
    // (FB-42) — without this, primary text is silently invisible when set
    // via property binding (the canonical authoring pattern per USAGE.md).
    const textEl = this.#ownChild('[slot="text"]');
    if (this.text) {
      if (textEl) {
        if (this.#wasStamped(textEl)) textEl.textContent = this.text;
      } else {
        const el = this.#stampMark(document.createElement('span'));
        el.setAttribute('slot', 'text');
        el.textContent = this.text;
        this.appendChild(el);
      }
    } else if (this.#wasStamped(textEl)) {
      textEl.remove();
    }

    // Sync description — only touch elements we stamped.
    const descEl = this.#ownChild('[slot="description"]');
    if (this.description) {
      if (descEl) {
        if (this.#wasStamped(descEl)) descEl.textContent = this.description;
      } else {
        const el = this.#stampMark(document.createElement('span'));
        el.setAttribute('slot', 'description');
        el.textContent = this.description;
        this.appendChild(el);
      }
    } else if (this.#wasStamped(descEl)) {
      descEl.remove();
    }
  }
}
