/**
 * Non-side-effect class export for `<command-ui>`.
 *
 * 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/command`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

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

/**
 * <command-ui> — Searchable command palette.
 *
 * Usage:
 *   <command-ui placeholder="Search...">
 *     <optgroup label="Navigation">
 *       <option value="home" data-icon="home" data-shortcut="⌘H">Go Home</option>
 *     </optgroup>
 *   </command-ui>
 *
 * Attributes on <option>:
 *   data-icon      — icon name (renders icon-ui)
 *   data-shortcut  — keyboard shortcut hint
 *   data-keywords  — extra searchable text
 *   disabled       — skips in navigation
 *
 * Events:
 *   select  — { detail: { value, label } }
 *   dismiss — Escape pressed
 */
export class UICommand extends UIElement {
  // §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
  // consumer markup). Aggregated by installIconLoadersForRegistered()
  // across all defined elements. Audited by check-required-icons.mjs
  // (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
  static requiredIcons = ['magnifying-glass'];

  static properties = {
    placeholder: { type: String, default: 'Type a command...', reflect: true },
    open:        { type: Boolean, default: false, reflect: true },
  };

  static template = () => null;

  #items = [];
  #recents = [];
  #activeIdx = -1;
  #inputEl = null;
  #listEl = null;
  #focusRaf = null;
  #footerEl = null;
  #bound = false;
  #itemByEl = new WeakMap();

  static #RECENTS_MAX = 3;

  #onListClick = (e) => {
    const el = e.target instanceof Element ? e.target.closest('[role="option"]:not([aria-disabled])') : null;
    if (!el) return;
    const item = this.#itemByEl.get(el);
    if (item) this.#select(item);
  };

  #onListPointerOver = (e) => {
    const el = e.target instanceof Element ? e.target.closest('[role="option"]:not([aria-disabled])') : null;
    if (!el) return;
    const idx = parseInt(el.dataset.idx);
    if (Number.isFinite(idx) && idx !== this.#activeIdx) this.#activate(idx);
  };

  connected() {
    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('keydown', this.#onKeydown);
    }
  }

  render() {
    // Stamp internal structure once
    if (!this.#inputEl) {
      // Parse options from declarative children before replacing innerHTML
      this.#parseOptions();

      this.innerHTML = `
        <header>
          <icon-ui name="magnifying-glass" slot="icon"></icon-ui>
          <input type="text" placeholder="${this.placeholder}" slot="input" />
        </header>
        <section slot="list"></section>
        <footer>
          <span data-hint><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
          <span data-hint><kbd>↵</kbd> Select</span>
          <span data-hint><kbd>⎋</kbd> Close</span>
        </footer>
      `;

      this.#inputEl = this.querySelector('input');
      this.#listEl = this.querySelector('[slot="list"]');
      this.#footerEl = this.querySelector('footer');

      this.#inputEl.addEventListener('input', this.#onInput);
      // Delegated item handlers — handler identity is stable so teardown is symmetric.
      this.#listEl.addEventListener('click', this.#onListClick);
      this.#listEl.addEventListener('pointerover', this.#onListPointerOver);
      this.#renderItems();
    }

    if (this.open) {
      this.#focusRaf = requestAnimationFrame(() => {
        this.#focusRaf = null;
        this.#inputEl?.focus();
      });
    }
  }

  // ── Public API ──

  get value() { return this.#inputEl?.value || ''; }
  set value(v) {
    if (!this.#inputEl) return;
    const next = v ?? '';
    this.#inputEl.value = next;
    // Keep the rendered list in sync with the visible input — the host
    // commonly clears value on (re)open, and consumers expect to see the
    // full list (plus recents) rather than the previous filter's residue.
    this.#renderItems(next);
  }

  focus() { this.#inputEl?.focus(); }

  setItems(items) {
    this.#items = items;
    this.#renderItems();
  }

  // ── Parse declarative <option>/<optgroup> ──

  // Wrapper-piercing walk for consumer-authored <option>/<optgroup> children.
  // Mirrors select-ui's #logicalOptionChildren (FB-98): .map()/repeat()-rendered
  // options arrive in display:contents/role="presentation" wrapper spans; a plain
  // this.children iteration misses them (FB-98 class, 5th sighting in command-ui).
  #logicalOptionChildren() {
    const out = [];
    const walk = (parent) => {
      for (const ch of parent.children) {
        if (ch.hasAttribute('slot')) continue;            // component-stamped — skip
        if (ch.tagName === 'OPTION' || ch.tagName === 'OPTGROUP') { out.push(ch); continue; }
        if (ch.matches('[role="presentation"]') || ch.style?.display === 'contents') { walk(ch); continue; }
        // unknown authored child — ignore (command-ui only uses option/optgroup)
      }
    };
    walk(this);
    return out;
  }

  #parseOptions() {
    const items = [];
    for (const child of this.#logicalOptionChildren()) {
      if (child.tagName === 'OPTGROUP') {
        const groupLabel = child.label;
        const group = { label: groupLabel, items: [] };
        for (const opt of child.querySelectorAll('option')) {
          group.items.push(this.#optionToItem(opt, groupLabel));
        }
        items.push(group);
      } else if (child.tagName === 'OPTION') {
        items.push(this.#optionToItem(child, ''));
      }
    }
    this.#items = items;
  }

  // §-TBD (v0.5.19, FB-50 #3): forward the parent <optgroup label> as
  // `category` on each item so the `select` event detail can surface
  // which group the chosen item came from. Items without a group
  // (direct <option> children of <command-ui>) get category = ''.
  #optionToItem(opt, category = '') {
    return {
      value: opt.value,
      label: opt.textContent.trim(),
      icon: opt.dataset.icon || '',
      shortcut: opt.dataset.shortcut || '',
      keywords: opt.dataset.keywords || '',
      disabled: opt.disabled,
      category,
    };
  }

  // ── Render ──

  #renderItems(filter = '') {
    if (!this.#listEl) return;
    this.#listEl.innerHTML = '';
    this.#activeIdx = -1;
    const q = filter.toLowerCase();

    const flat = this.#items;
    let visibleCount = 0;

    // Recents — only when no filter is applied. Resolved against the
    // current item set so a stale recent (item removed via setItems)
    // is silently dropped.
    if (!q && this.#recents.length) {
      const resolved = this.#recents
        .map(v => this.#findItem(v))
        .filter(it => it && !it.disabled);
      if (resolved.length) {
        const groupEl = document.createElement('div');
        groupEl.setAttribute('data-group', '');
        groupEl.setAttribute('data-recent', '');
        groupEl.innerHTML = `<div data-group-label>Recent</div>`;
        for (const item of resolved) {
          groupEl.appendChild(this.#createItemEl(item, visibleCount));
          visibleCount++;
        }
        this.#listEl.appendChild(groupEl);
      }
    }

    for (const entry of flat) {
      if (entry.items) {
        // Group
        const filtered = entry.items.filter(i => this.#matches(i, q));
        if (!filtered.length) continue;

        const groupEl = document.createElement('div');
        groupEl.setAttribute('data-group', '');
        groupEl.innerHTML = `<div data-group-label>${entry.label}</div>`;

        for (const item of filtered) {
          groupEl.appendChild(this.#createItemEl(item, visibleCount));
          visibleCount++;
        }
        this.#listEl.appendChild(groupEl);
      } else {
        if (!this.#matches(entry, q)) continue;
        this.#listEl.appendChild(this.#createItemEl(entry, visibleCount));
        visibleCount++;
      }
    }

    // Empty state
    if (!visibleCount) {
      const empty = document.createElement('div');
      empty.setAttribute('data-empty', '');
      empty.textContent = 'No results found.';
      this.#listEl.appendChild(empty);
    }

    // Activate first non-disabled
    if (visibleCount > 0) this.#activate(0);
  }

  #findItem(value) {
    for (const entry of this.#items) {
      if (entry.items) {
        const hit = entry.items.find(i => i.value === value);
        if (hit) return hit;
      } else if (entry.value === value) {
        return entry;
      }
    }
    return null;
  }

  #pushRecent(value) {
    if (!value) return;
    this.#recents = [value, ...this.#recents.filter(v => v !== value)]
      .slice(0, UICommand.#RECENTS_MAX);
  }

  #createItemEl(item, idx) {
    const el = document.createElement('div');
    el.setAttribute('role', 'option');
    el.setAttribute('data-value', item.value);
    el.setAttribute('data-idx', idx);
    if (item.disabled) el.setAttribute('aria-disabled', 'true');

    el.innerHTML = `
      ${item.icon ? `<icon-ui name="${item.icon}"></icon-ui>` : ''}
      <span data-text>${item.label}</span>
      ${item.shortcut ? `<span data-shortcut>${item.shortcut}</span>` : ''}
    `;

    this.#itemByEl.set(el, item);
    return el;
  }

  #matches(item, q) {
    if (!q) return true;
    return (item.label || '').toLowerCase().includes(q) ||
           (item.value || '').toLowerCase().includes(q) ||
           (item.keywords || '').toLowerCase().includes(q);
  }

  // ── Navigation ──

  #activate(idx) {
    const options = this.#listEl.querySelectorAll('[role="option"]:not([aria-disabled])');
    const prev = this.#listEl.querySelector('[data-active]');
    if (prev) prev.removeAttribute('data-active');

    const target = [...options].find(el => parseInt(el.dataset.idx) === idx);
    if (target) {
      target.setAttribute('data-active', '');
      target.scrollIntoView({ block: 'nearest' });
      this.#activeIdx = idx;
    }
  }

  #moveActive(dir) {
    const options = [...this.#listEl.querySelectorAll('[role="option"]:not([aria-disabled])')];
    if (!options.length) return;

    const currentEl = this.#listEl.querySelector('[data-active]');
    const currentPos = currentEl ? options.indexOf(currentEl) : -1;
    let next = currentPos + dir;
    if (next < 0) next = options.length - 1;
    if (next >= options.length) next = 0;

    this.#activate(parseInt(options[next].dataset.idx));
  }

  // §-TBD (v0.5.19, FB-50 #3): emit `category` (from parent <optgroup
   // label>) in the select detail so consumers can route on group.
   // Items without a group surface category = ''.
  #select(item) {
    this.#pushRecent(item.value);
    this.dispatchEvent(new CustomEvent('select', {
      bubbles: true,
      detail: {
        value: item.value,
        label: item.label,
        category: item.category || '',
      },
    }));
  }

  #selectActive() {
    const active = this.#listEl.querySelector('[data-active]');
    if (!active) return;
    const value = active.dataset.value;
    const label = active.querySelector('[data-text]')?.textContent || '';
    // Look up the item to surface category — the rendered DOM doesn't
    // carry the group label, but #items / #itemByEl do.
    const item = this.#itemByEl.get(active) || this.#findItem(value);
    const category = item?.category || '';
    this.#pushRecent(value);
    this.dispatchEvent(new CustomEvent('select', {
      bubbles: true,
      detail: { value, label, category },
    }));
  }

  // ── Events ──

  #onInput = () => {
    this.#renderItems(this.#inputEl.value);
  };

  #onKeydown = (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        this.#moveActive(1);
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.#moveActive(-1);
        break;
      case 'Enter':
        e.preventDefault();
        this.#selectActive();
        break;
      case 'Tab':
        e.preventDefault();
        this.#selectActive();
        break;
      case 'Escape':
        e.preventDefault();
        this.dispatchEvent(new Event('dismiss', { bubbles: true }));
        break;
    }
  };

  disconnected() {
    if (this.#focusRaf != null) {
      cancelAnimationFrame(this.#focusRaf);
      this.#focusRaf = null;
    }
    this.removeEventListener('keydown', this.#onKeydown);
    this.#inputEl?.removeEventListener('input', this.#onInput);
    this.#listEl?.removeEventListener('click', this.#onListClick);
    this.#listEl?.removeEventListener('pointerover', this.#onListPointerOver);
    this.#inputEl = null;
    this.#listEl = null;
    this.#footerEl = null;
    this.#bound = false;
  }
}
