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

/**
 * <segmented-ui value="tab1">
 *   <segment-ui value="tab1" text="Tab 1"></segment-ui>
 *   <segment-ui value="tab2" text="Tab 2"></segment-ui>
 *   <segment-ui value="tab3" text="Tab 3"></segment-ui>
 * </segmented-ui>
 *
 * Mutually exclusive toggle group with animated indicator pill.
 * Indicator uses CSS transform (GPU-accelerated).
 */

import { UIFormElement } from '../../core/form.js';
import '../segment/segment.js';

export class UISegmented extends UIFormElement {
  static properties = {
    ...UIFormElement.properties,
    value: { type: String, default: '', reflect: true },
  };

  static template = () => null;

  // FEEDBACK-23: one-shot per-element warn when a consumer authors a direct
  // child that isn't <segment-ui> (typically a bare <segment> tag — renders
  // text but gets no sliding indicator, role, or aria-checked). WeakSet so
  // the warn never pins the element from GC.
  static #warnedNonSegment = new WeakSet();

  #indicator = null;
  #bound = false;
  #transitionRaf = null;
  #resizeObs = null;
  #fontWait = null;

  connected() {
    super.connected();
    this.setAttribute('role', 'radiogroup');

    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('click', this.#handleClick);
      this.addEventListener('keydown', this.#handleKeydown);
    }

    // Default to first segment if no value
    if (!this.value) {
      const first = this.querySelector('segment-ui:not([disabled])');
      if (first) this.value = first.value || first.getAttribute('value') || '';
    }

    // Recalculate indicator when layout shifts (font load, resize, content change)
    if (typeof ResizeObserver !== 'undefined') {
      this.#resizeObs = new ResizeObserver(() => this.#updateIndicator(this.#segments));
      this.#resizeObs.observe(this);
    }

    // Also wait for fonts to settle — a common source of initial-layout drift
    if (document.fonts?.ready) {
      this.#fontWait = document.fonts.ready.then(() => {
        this.#fontWait = null;
        this.#updateIndicator(this.#segments);
      });
    }
  }

  disconnected() {
    super.disconnected();
    if (this.#transitionRaf != null) {
      cancelAnimationFrame(this.#transitionRaf);
      this.#transitionRaf = null;
    }
    this.#resizeObs?.disconnect();
    this.#resizeObs = null;
    if (this.#fontWait) {
      // Promise can't be cancelled; the then-handler checks this.isConnected
      this.#fontWait = null;
    }
    this.removeEventListener('click', this.#handleClick);
    this.removeEventListener('keydown', this.#handleKeydown);
    this.#indicator = null;
    this.#bound = false;
  }

  get #segments() {
    return [...this.querySelectorAll('segment-ui')];
  }

  get #enabledSegments() {
    return this.#segments.filter(s => !s.disabled);
  }

  render() {
    // FEEDBACK-23: flag a non-<segment-ui> direct child once per element.
    // Runs before the empty-guard so an all-bare-<segment> group still warns.
    if (!UISegmented.#warnedNonSegment.has(this)) {
      const bad = [...this.children].find(
        (c) => c.tagName !== 'SEGMENT-UI'
            && !c.hasAttribute('data-indicator')
            // Template engine wraps .map() results in <span style="display:contents">.
            // These are transparent DOM housekeeping nodes — not consumer mistakes.
            && !(c.tagName === 'SPAN' && c.style.display === 'contents'),
      );
      if (bad) {
        UISegmented.#warnedNonSegment.add(this);
        // eslint-disable-next-line no-console
        console.warn(
          `[segmented-ui] child <${bad.tagName.toLowerCase()}> is not ` +
          `<segment-ui> — bare <segment> tags render text but receive no ` +
          `sliding indicator or aria-checked state. Use <segment-ui>.`,
        );
      }
    }

    const segs = this.#segments;
    if (!segs.length) return;

    // Sync selection state
    for (const seg of segs) {
      const segVal = seg.value || seg.getAttribute('value') || '';
      const isActive = segVal === this.value;
      if (isActive) {
        seg.setAttribute('selected', '');
      } else {
        seg.removeAttribute('selected');
      }
      seg.setAttribute('tabindex', isActive ? '0' : '-1');
    }

    if (this.label) this.setAttribute('aria-label', this.label);

    this.#updateIndicator(segs);
  }

  #updateIndicator(segs) {
    const selectedIdx = segs.findIndex(s => (s.value || s.getAttribute('value') || '') === this.value);

    if (selectedIdx < 0) {
      this.removeAttribute('data-indicator-ready');
      return;
    }

    // Indicator — there must be EXACTLY ONE [data-indicator] child at all
    // times. If the instance ref is missing OR detached, strip every stale
    // indicator from the DOM (defensive: re-renders after disconnect/
    // reconnect cycles, HMR, or stamp() can leave the field nulled while
    // the previous indicator span persists in DOM → multiple translateX'd
    // pills paint simultaneously). Then create a fresh one.
    const isNew = !this.#indicator || !this.contains(this.#indicator);
    if (isNew) {
      for (const stale of this.querySelectorAll(':scope > [data-indicator]')) {
        stale.remove();
      }
      this.#indicator = document.createElement('span');
      this.#indicator.setAttribute('data-indicator', '');
      this.prepend(this.#indicator);
    }

    this.style.setProperty('--_segment-count', `${segs.length}`);

    if (isNew) {
      // First render: position instantly, then enable transition next frame
      this.#indicator.style.transition = 'none';
      this.#indicator.style.transform = `translateX(${selectedIdx * 100}%)`;
      this.setAttribute('data-indicator-ready', '');
      this.#transitionRaf = requestAnimationFrame(() => {
        this.#transitionRaf = null;
        if (this.#indicator) this.#indicator.style.transition = '';
      });
    } else {
      this.#indicator.style.transform = `translateX(${selectedIdx * 100}%)`;
      this.setAttribute('data-indicator-ready', '');
    }
  }

  #select(seg) {
    if (seg.disabled) return;
    this.value = seg.value;
    this.syncValue();
    this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
  }

  #handleClick = (e) => {
    const seg = e.target.closest('segment-ui');
    if (seg && this.contains(seg)) this.#select(seg);
  };

  #handleKeydown = (e) => {
    const target = e.target;
    if (target.tagName !== 'SEGMENT-UI') return;
    const segs = this.#enabledSegments;
    const idx = segs.indexOf(target);
    if (idx < 0) return;

    let next;
    switch (e.key) {
      case 'ArrowRight': case 'ArrowDown':
        next = idx < segs.length - 1 ? idx + 1 : 0; break;
      case 'ArrowLeft': case 'ArrowUp':
        next = idx > 0 ? idx - 1 : segs.length - 1; break;
      case 'Home': next = 0; break;
      case 'End': next = segs.length - 1; break;
      default: return;
    }

    e.preventDefault();
    segs[next].focus();
    this.#select(segs[next]);
  };
}
