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

/**
 * <toggle-scheme-ui>
 *
 * Icon-button primitive that toggles `color-scheme` between light and dark
 * on a target element (default `:root`). Replaces the hand-wired
 * `<button-ui id="theme-toggle"> + applyScheme()` boilerplate previously
 * duplicated across apps/genui, apps/construct-canvas, playgrounds/chat.
 *
 * Composition: light-DOM host, internally stamps a single <button-ui>.
 *
 * Authoring API:
 *   [scheme="auto"|"light"|"dark"]   initial scheme (default "auto")
 *   [target=":root"]                 CSS selector for target element
 *   [persist]                        write selection to localStorage
 *   [storage-prefix="adia-theme-"]   LS key namespace (matches theme-panel)
 *   [icon-light="sun"]               icon shown when active-scheme is light
 *   [icon-dark="moon"]               icon shown when active-scheme is dark
 *   [label-light]                    aria-label / title in light mode
 *   [label-dark]                     aria-label / title in dark mode
 *   [size="md"]                      forwarded to internal <button-ui>
 *   [variant="ghost"]                forwarded to internal <button-ui>
 *   [color=""]                       forwarded to internal <button-ui>
 *   [disabled]                       forwarded to internal <button-ui>
 *
 * Reflected state (read-only externally; use .setScheme() to mutate):
 *   [active-scheme]   resolved scheme ("light" | "dark")
 *
 * Events:
 *   scheme-change — bubbles. detail: { scheme, source }
 *     source ∈ "press" | "media" | "programmatic"
 *
 * Public methods:
 *   .toggle()                 flip between light and dark (defeats auto)
 *   .setScheme(s)             "light" | "dark" | "auto"; programmatic write
 */

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

const LIGHT = 'light';
const DARK  = 'dark';
const AUTO  = 'auto';

export class UIToggleScheme extends UIElement {
  static properties = {
    scheme:        { type: String,  default: AUTO,   reflect: true },
    target:        { type: String,  default: ':root', reflect: true },
    persist:       { type: Boolean, default: false, reflect: true },
    storagePrefix: { type: String,  default: 'adia-theme-', reflect: true, attribute: 'storage-prefix' },
    iconLight:     { type: String,  default: 'sun',  reflect: true, attribute: 'icon-light' },
    iconDark:      { type: String,  default: 'moon', reflect: true, attribute: 'icon-dark' },
    labelLight:    { type: String,  default: 'Switch to dark mode',  reflect: true, attribute: 'label-light' },
    labelDark:     { type: String,  default: 'Switch to light mode', reflect: true, attribute: 'label-dark' },
    size:          { type: String,  default: 'md',    reflect: true },
    variant:       { type: String,  default: 'ghost', reflect: true },
    color:         { type: String,  default: '',      reflect: true },
    disabled:      { type: Boolean, default: false,   reflect: true },
    activeScheme:  { type: String,  default: '',      reflect: true, attribute: 'active-scheme' },
  };

  static template = () => null;

  #button = null;
  #mql = null;
  #mqlHandler = null;
  #onPress = null;
  #stamped = false;
  // §200 (v0.5.7, FEEDBACK-10 §1): set true after first user-driven scheme
  // mutation (button press OR programmatic setScheme/toggle). Until then,
  // the `scheme` attribute is treated as "reactive consumer-driven" — any
  // post-connect attribute application re-runs #initState() so the reactive
  // value wins over the template-strip race.
  #userTouched = false;

  connected() {
    if (!this.#stamped) {
      this.#stamp();
      this.#stamped = true;
    }
    this.#initState();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    // §200 (v0.5.7, FEEDBACK-10 §1): UIElement.attributeChangedCallback syncs
    // attr → property. After it runs, if the `scheme` attribute changed AFTER
    // connect AND the user hasn't yet chosen explicitly, re-run #initState()
    // so the reactive consumer value wins over the template-engine's strip-
    // then-restamp race. Once the user clicks the button or calls
    // setScheme()/toggle() programmatically, #userTouched flips true and we
    // stop auto-reinit so user choice survives subsequent re-renders.
    super.attributeChangedCallback(name, oldVal, newVal);
    if (
      name === 'scheme' &&
      this.isConnected &&
      this.#stamped &&
      !this.#userTouched &&
      oldVal !== newVal
    ) {
      this.#initState();
    }
  }

  disconnected() {
    if (this.#button && this.#onPress) {
      this.#button.removeEventListener('press', this.#onPress);
    }
    this.#detachPcm();
  }

  // ── Public API ──────────────────────────────────────────────────────

  /** Flip between light and dark — defeats auto. */
  toggle() {
    this.#userTouched = true;
    const next = this.activeScheme === DARK ? LIGHT : DARK;
    this.#apply(next, 'programmatic');
  }

  /**
   * @param {"light"|"dark"|"auto"} s
   */
  setScheme(s) {
    this.#userTouched = true;
    if (s === AUTO) {
      this.#clearTargetOverride();
      const resolved = this.#resolvePrefersScheme();
      this.activeScheme = resolved;
      this.#syncButton();
      this.#attachPcm();
      this.#emit('programmatic');
    } else if (s === LIGHT || s === DARK) {
      this.#apply(s, 'programmatic');
    }
  }

  // ── Internal — stamping ─────────────────────────────────────────────

  #stamp() {
    const btn = document.createElement('button-ui');
    btn.setAttribute('part', 'button');
    btn.setAttribute('variant', this.variant || 'ghost');
    btn.setAttribute('size', this.size || 'md');
    if (this.color) btn.setAttribute('color', this.color);
    if (this.disabled) btn.setAttribute('disabled', '');
    this.appendChild(btn);

    this.#button = btn;
    this.#onPress = (e) => {
      // Internal button bubbles 'press'; we re-emit our own 'scheme-change'
      // so consumers see one semantic event, not the inner button's.
      e.stopPropagation();
      if (this.disabled) return;
      // §200 (v0.5.7): mark user-touched so post-connect reactive `scheme`
      // attribute writes don't override the user's explicit choice.
      this.#userTouched = true;
      const next = this.activeScheme === DARK ? LIGHT : DARK;
      this.#apply(next, 'press');
    };
    btn.addEventListener('press', this.#onPress);
  }

  // ── Internal — initial state ────────────────────────────────────────

  #initState() {
    // Priority: persisted > [scheme] attr > auto
    let initial = null;
    if (this.persist) {
      try {
        const v = localStorage.getItem(`${this.storagePrefix}scheme`);
        if (v === LIGHT || v === DARK) initial = v;
      } catch {}
    }
    if (initial === null) {
      const attr = (this.scheme || AUTO);
      if (attr === LIGHT || attr === DARK) initial = attr;
    }

    if (initial === LIGHT || initial === DARK) {
      // Explicit choice — write to target, no MQL listening
      this.#writeTarget(initial);
      this.activeScheme = initial;
    } else {
      // Auto — follow prefers-color-scheme, attach listener
      this.activeScheme = this.#resolvePrefersScheme();
      this.#attachPcm();
    }
    this.#syncButton();
  }

  // ── Internal — mutation (single path) ───────────────────────────────

  #apply(next, source) {
    if (next !== LIGHT && next !== DARK) return;
    this.#writeTarget(next);
    this.activeScheme = next;
    if (this.persist) {
      try { localStorage.setItem(`${this.storagePrefix}scheme`, next); } catch {}
    }
    // User picked explicitly — stop tracking OS preference
    this.#detachPcm();
    this.#syncButton();
    this.#emit(source);
  }

  #syncButton() {
    if (!this.#button) return;
    const dark = this.activeScheme === DARK;
    const icon = dark ? this.iconDark : this.iconLight;
    const label = dark ? this.labelDark : this.labelLight;
    if (this.#button.getAttribute('icon') !== icon) {
      this.#button.setAttribute('icon', icon);
    }
    this.#button.setAttribute('aria-label', label);
    this.#button.setAttribute('title', label);
    // Forward style passthrough so authors can change variant/size/color
    // at runtime and the internal button reflects it.
    if (this.#button.getAttribute('variant') !== this.variant) {
      this.#button.setAttribute('variant', this.variant || 'ghost');
    }
    if (this.#button.getAttribute('size') !== this.size) {
      this.#button.setAttribute('size', this.size || 'md');
    }
    if (this.color) {
      this.#button.setAttribute('color', this.color);
    } else {
      this.#button.removeAttribute('color');
    }
    if (this.disabled) {
      this.#button.setAttribute('disabled', '');
    } else {
      this.#button.removeAttribute('disabled');
    }
  }

  render() {
    // Property writes after connect (e.g. el.size = 'sm') route through here.
    this.#syncButton();
  }

  #emit(source) {
    this.dispatchEvent(new CustomEvent('scheme-change', {
      bubbles: true,
      detail: { scheme: this.activeScheme, source },
    }));
  }

  // ── Internal — target resolution + write ────────────────────────────

  #resolveTarget() {
    const sel = this.target || ':root';
    if (sel === ':root') return document.documentElement;
    try {
      return document.querySelector(sel) ?? document.documentElement;
    } catch {
      return document.documentElement;
    }
  }

  #writeTarget(scheme) {
    const t = this.#resolveTarget();
    t.style.setProperty('color-scheme', scheme);
    // FEEDBACK-14: also write the [data-scheme] attribute. styles/themes.css
    // selects on [data-scheme="dark"] etc., so consumer CSS patterned after
    // themes.css needs the attribute — the inline color-scheme style alone
    // satisfies AdiaUI's light-dark() tokens but not [data-scheme]-scoped CSS.
    t.setAttribute('data-scheme', scheme);
  }

  #clearTargetOverride() {
    const t = this.#resolveTarget();
    t.style.removeProperty('color-scheme');
    t.removeAttribute('data-scheme'); // FEEDBACK-14 — paired with #writeTarget
    if (this.persist) {
      try { localStorage.removeItem(`${this.storagePrefix}scheme`); } catch {}
    }
  }

  // ── Internal — prefers-color-scheme ─────────────────────────────────

  #resolvePrefersScheme() {
    try {
      if (typeof matchMedia === 'function' &&
          matchMedia('(prefers-color-scheme: dark)').matches) return DARK;
    } catch {}
    return LIGHT;
  }

  #attachPcm() {
    if (this.#mql || typeof matchMedia !== 'function') return;
    try {
      this.#mql = matchMedia('(prefers-color-scheme: dark)');
      this.#mqlHandler = () => {
        const resolved = this.#mql.matches ? DARK : LIGHT;
        if (resolved === this.activeScheme) return;
        this.activeScheme = resolved;
        this.#syncButton();
        this.#emit('media');
      };
      this.#mql.addEventListener('change', this.#mqlHandler);
    } catch {}
  }

  #detachPcm() {
    if (this.#mql && this.#mqlHandler) {
      try { this.#mql.removeEventListener('change', this.#mqlHandler); } catch {}
    }
    this.#mql = null;
    this.#mqlHandler = null;
  }
}
