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

/**
 * <color-input-ui value="#3b82f6" format="hex" name="brand"></color-input-ui>
 *
 * Compact form-bearing color input. §302 (v0.5.12, FEEDBACK-29 re-bucket).
 *
 * Canonicalizes the USAGE.md §221f recipe (popover + button + color-picker)
 * into a single form-associated tag. Builds its inner DOM in `connected()`
 * as light-DOM children:
 *
 *   <color-input-ui>
 *     <popover-ui trigger="click" placement="bottom-start">
 *       <button-ui slot="trigger" variant="outline">
 *         <span class="color-input__swatch"></span>
 *         <span class="color-input__value"></span>
 *       </button-ui>
 *       <color-picker-ui slot="content"></color-picker-ui>
 *     </popover-ui>
 *   </color-input-ui>
 *
 * Form-associated via UIFormElement + ElementInternals.
 */

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

export class UIColorInput extends UIFormElement {
  static get properties() {
    return {
      ...UIFormElement.properties,
      value:       { type: String,  default: '#3b82f6', reflect: true },
      format:      { type: String,  default: 'hex', reflect: true },
      placement:   { type: String,  default: 'bottom', reflect: true }, // ADR-0034 Rule 2: color picker panel >> swatch trigger
      open:        { type: Boolean, default: false, reflect: true },
      // Generation-constraint props forwarded to the inner <color-picker-ui>.
      // v0.5.13 §-TBD (FB-33 §1) — full parity with color-picker's 5 constraint
      // props (`maxChroma` / `maxL` / `minL` / `hueDriftMax` / `baseHue`) so
      // <color-input-ui> adoption doesn't force a UX regression on consumers
      // already using L-range / chroma / hue-drift constraints.
      maxChroma:   { type: Number,  default: Infinity, reflect: true },
      maxL:        { type: Number,  default: 1, reflect: true },
      minL:        { type: Number,  default: 0, reflect: true },
      hueDriftMax: { type: Number,  default: NaN, reflect: true },
      baseHue:     { type: Number,  default: NaN, reflect: true },
    };
  }

  #popover = null;
  #button = null;
  #picker = null;
  #swatch = null;
  #valueLabel = null;
  #wired = false;
  #popoverObserver = null;

  connected() {
    this.#mount();
    this.#wire();
    this.#syncFromValue();
    this.syncValue(this.value);
  }

  render() {
    this.#syncFromValue();
    if (this.#popover && this.#popover.open !== this.open) {
      this.#popover.open = this.open;
    }
    if (this.#picker) {
      if (this.#picker.format !== this.format) this.#picker.format = this.format;
      // Forward live changes to the 5 generation-constraint props (FB-33 §1).
      // Object.is handles the NaN-equality case correctly so `baseHue`/
      // `hueDriftMax` don't reassign on every render.
      if (!Object.is(this.#picker.maxChroma, this.maxChroma))   this.#picker.maxChroma = this.maxChroma;
      if (!Object.is(this.#picker.maxL, this.maxL))             this.#picker.maxL = this.maxL;
      if (!Object.is(this.#picker.minL, this.minL))             this.#picker.minL = this.minL;
      if (!Object.is(this.#picker.hueDriftMax, this.hueDriftMax)) this.#picker.hueDriftMax = this.hueDriftMax;
      if (!Object.is(this.#picker.baseHue, this.baseHue))       this.#picker.baseHue = this.baseHue;
    }
    if (this.#button) {
      if (this.disabled) this.#button.setAttribute('disabled', '');
      else this.#button.removeAttribute('disabled');
    }
  }

  // ── Public API ──

  showPicker() { this.open = true; }
  hidePicker() { this.open = false; }

  // ── Internal ──

  #mount() {
    if (this.firstElementChild) {
      this.#popover = this.querySelector(':scope > popover-ui');
      this.#button = this.querySelector(':scope > popover-ui > button-ui[slot="trigger"]');
      this.#picker = this.querySelector(':scope > popover-ui > color-picker-ui[slot="content"]');
      this.#swatch = this.querySelector('.color-input__swatch');
      this.#valueLabel = this.querySelector('.color-input__value');
      if (this.#popover && this.#button && this.#picker) return;
    }

    const popover = document.createElement('popover-ui');
    popover.setAttribute('trigger', 'click');
    popover.setAttribute('placement', this.placement);

    const button = document.createElement('button-ui');
    button.setAttribute('slot', 'trigger');
    button.setAttribute('variant', 'outline');
    button.setAttribute('aria-haspopup', 'dialog');
    button.setAttribute('aria-label', this.getAttribute('aria-label') || 'Pick a color');

    const swatch = document.createElement('span');
    swatch.className = 'color-input__swatch';
    swatch.setAttribute('aria-hidden', 'true');

    const valueLabel = document.createElement('span');
    valueLabel.className = 'color-input__value';

    button.append(swatch, valueLabel);

    const picker = document.createElement('color-picker-ui');
    picker.setAttribute('slot', 'content');
    picker.setAttribute('format', this.format);
    picker.setAttribute('value', this.value);
    // Set generation-constraint props directly (numeric — attribute-reflection
    // would round-trip but properties cover NaN/Infinity correctly).
    picker.maxChroma = this.maxChroma;
    picker.maxL = this.maxL;
    picker.minL = this.minL;
    picker.hueDriftMax = this.hueDriftMax;
    picker.baseHue = this.baseHue;

    popover.append(button, picker);
    this.append(popover);

    this.#popover = popover;
    this.#button = button;
    this.#picker = picker;
    this.#swatch = swatch;
    this.#valueLabel = valueLabel;
  }

  #wire() {
    if (this.#wired) return;
    this.#wired = true;

    this.#picker.addEventListener('change', this.#onPickerChange);
    this.#picker.addEventListener('input', this.#onPickerInput);

    // Track popover open/close so the host's `open` prop reflects reality
    // even when the user dismisses via ESC / outside-click.
    const sync = () => {
      const open = !!this.#popover?.open;
      if (this.open !== open) this.open = open;
    };
    this.#popover.addEventListener('toggle', sync);
    // Store the observer in a #field so disconnected() can clean it up
    // — without the explicit reference, the observer survives the host
    // element across mount/unmount cycles (audit-lifecycle-leak).
    this.#popoverObserver = new MutationObserver(sync);
    this.#popoverObserver.observe(this.#popover, {
      attributes: true,
      attributeFilter: ['open'],
    });
  }

  #onPickerChange = (e) => {
    this.#commit(e.detail, 'change');
  };

  #onPickerInput = (e) => {
    this.#commit(e.detail, 'input');
  };

  #commit(detail, kind) {
    if (!detail) return;
    const next = this.format === 'oklch' ? detail.oklch : detail.hex;
    if (next && next !== this.value) {
      this.value = next;
      this.syncValue(next);
    }
    this.#syncFromValue();
    // Forward the inner picker's parsed OKLCH channel scalars (v0.5.13 §-TBD,
    // FB-33 §1-adjacent). Consumers writing OKLCH-native logic get the parsed
    // {l, c, h} for free, matching <color-picker-ui>'s detail shape.
    this.dispatchEvent(new CustomEvent(kind, {
      bubbles: true,
      composed: true,
      detail: {
        value: next ?? this.value,
        hex: detail.hex,
        oklch: detail.oklch,
        l: detail.l,
        c: detail.c,
        h: detail.h,
      },
    }));
  }

  #syncFromValue() {
    if (this.#swatch) {
      this.#swatch.style.setProperty('--swatch-color', this.value);
    }
    if (this.#valueLabel) {
      this.#valueLabel.textContent = this.value;
    }
  }

  disconnected() {
    this.#picker?.removeEventListener('change', this.#onPickerChange);
    this.#picker?.removeEventListener('input', this.#onPickerInput);
    this.#popoverObserver?.disconnect();
    this.#popoverObserver = null;
    this.#wired = false;
  }
}
