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

/**
 * <accordion-ui>
 *   <accordion-item-ui text="Section 1">Content for section 1</accordion-item-ui>
 *   <accordion-item-ui text="Section 2">Content for section 2</accordion-item-ui>
 * </accordion-ui>
 *
 * Accordion container + item pair.
 * Container manages single/multiple open states.
 * Items handle their own expand/collapse toggle.
 *
 * Events (on accordion-item-ui):
 *   toggle — { detail: { open } }
 */

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

// ── Accordion Container ────────────────────────────────────────

export class UIAccordion 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 = ['caret-down'];

  static properties = {
    multiple: { type: Boolean, default: false, reflect: true },
    // `reflect: true` is load-bearing: the CSS ancestor selector
    // (accordion.css `@scope (accordion-item-ui)`) matches
    // `accordion-ui[variant="contained"]`. Default 'flat' ⇒ absent-attribute
    // = current behavior. No render/JS logic keys off it — inheritance of
    // the contained posture into child items is pure CSS.
    variant: { type: String, default: 'flat', reflect: true }, // 'flat' | 'contained'
  };

  static template = () => null;

  connected() {
    this.addEventListener('toggle', this.#onItemToggle);
  }

  disconnected() {
    this.removeEventListener('toggle', this.#onItemToggle);
  }

  #onItemToggle = (e) => {
    if (this.multiple) return;

    const target = e.target;
    if (!target.open) return;

    // Close other items
    const items = this.querySelectorAll('accordion-item-ui');
    for (const item of items) {
      if (item !== target && item.open) {
        item.open = false;
      }
    }
  };
}

export class UIAccordionItem extends UIElement {
  static properties = {
    text: { type: String,  default: '', reflect: true },
    open: { type: Boolean, default: false, reflect: true },
    // Per-item posture override against the host accordion-ui's [variant]
    // (see UIAccordion.variant above). Default 'flat'; CSS-only, pure
    // token re-point in contained (accordion.css).
    variant: { type: String, default: 'flat', reflect: true }, // 'flat' | 'contained'
  };

  static template = () => null;

  #bound = false;

  connected() {
    // Stamp header if not present; otherwise restore the disclosure caret
    // (FN-1) — a consumer's custom [slot="header"] previously suppressed it
    // entirely because the caret only ever got stamped alongside the header.
    const existingHeader = this.querySelector('[slot="header"]');
    if (!existingHeader) {
      const header = document.createElement('div');
      header.setAttribute('slot', 'header');
      // SPEC-R8: the auto-stamped header was mouse-only (no tabindex/keydown).
      // Make it keyboard-operable; a consumer-authored header keeps owning
      // its own semantics (data-autofocusable marks the header WE own).
      header.setAttribute('tabindex', '0');
      header.setAttribute('role', 'button');
      header.setAttribute('aria-expanded', 'false');
      header.dataset.autofocusable = '';

      const label = document.createElement('span');
      label.setAttribute('slot', 'header-text');
      label.dataset.autolabel = ''; // marks THIS label as prop-driven (FN-2)
      header.appendChild(label);

      const caret = document.createElement('icon-ui');
      caret.setAttribute('name', 'caret-down');
      caret.setAttribute('slot', 'caret');
      header.appendChild(caret);

      this.prepend(header);
    } else if (!existingHeader.querySelector('[slot="caret"]')) {
      // Scope to the HEADER, not the whole item — a caret placed in
      // [slot="body"] must not suppress this (Finding 10).
      const caret = document.createElement('icon-ui');
      caret.setAttribute('name', 'caret-down');
      caret.setAttribute('slot', 'caret');
      existingHeader.appendChild(caret); // restore the disclosure signifier
    }

    // Stamp content wrapper if not present
    if (!this.querySelector('[slot="body"]')) {
      const body = document.createElement('div');
      body.setAttribute('slot', 'body');

      // Move non-slotted children into body
      const children = [...this.childNodes].filter(n => {
        if (n.nodeType === 1) return !n.hasAttribute('slot');
        return n.nodeType === 3 && n.textContent.trim();
      });
      for (const child of children) body.appendChild(child);

      this.appendChild(body);
    }

    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() {
    // Only the auto-stamped label follows [text] — an authored
    // [slot="header-text"] (no data-autolabel marker) is never touched (FN-2).
    const label = this.querySelector('[slot="header-text"][data-autolabel]');
    if (label) label.textContent = this.text;

    // Keep aria-expanded in sync on the auto-stamped header — `open` is a
    // reactive prop, so render() re-runs on every toggle (SPEC-R8).
    const hdr = this.querySelector('[slot="header"][data-autofocusable]');
    if (hdr) hdr.setAttribute('aria-expanded', String(this.open));
  }

  #onClick = (e) => {
    const header = this.querySelector('[slot="header"]');
    if (!header || !header.contains(e.target)) return;
    // FEEDBACK-16 §2 (v0.5.7 §206): skip toggle when click originates inside an
    // action slot or an opt-out marker. Lets consumers author action buttons in
    // custom headers without bubbling-toggle UX. Slot vocabulary matches
    // drawer-ui + pane-ui.
    if (e.target.closest('[slot="action"], [slot="actions"], [data-no-toggle]')) return;

    this.open = !this.open;
    this.dispatchEvent(new CustomEvent('toggle', {
      bubbles: true,
      detail: { open: this.open },
    }));
  };

  // SPEC-R8: Enter/Space on the auto-stamped header toggles it. Scoped to
  // OUR stamped header only (data-autofocusable) — never a consumer control
  // inside a custom header. Routes through header.click() → #onClick, so
  // there is exactly one toggle code path (DRY).
  #onKeydown = (e) => {
    const header = this.querySelector('[slot="header"][data-autofocusable]');
    if (!header || e.target !== header) return;
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault(); // Space: no page scroll
      header.click();
    }
  };
}
