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

/**
 * <loading-overlay-ui active label="Loading orders…"></loading-overlay-ui>
 * <loading-overlay-ui active variant="transparent" delay="0">
 *   <skeleton-ui width="100%" height="1rem"></skeleton-ui>
 *   <skeleton-ui width="80%"  height="1rem"></skeleton-ui>
 * </loading-overlay-ui>
 *
 * Container-scoped busy overlay. Covers a positioned parent region with
 * a spinner, skeleton, or custom busy indicator while async work is in
 * flight. The host:
 *
 *   1. Absolutely positions to fill the offsetParent (consumer's
 *      responsibility to ensure that parent is `position: relative` or
 *      similar — documented in the spec / usage notes; we do NOT mutate
 *      parent layout styles).
 *   2. Applies aria-busy="true" to the parent on activate; releases on
 *      deactivate / disconnect. This is screen-reader politeness; the
 *      backdrop's pointer-events:auto handles the click/focus block at
 *      the CSS level (no native [inert] attribute toggling — that's
 *      kept minimal to avoid disturbing the consumer's focus model).
 *   3. Auto-stamps a centered <spinner-ui size="lg"> when the default
 *      slot is empty.
 *   4. Honors a `[delay]` grace window (default 200ms) — if the active
 *      flag is cleared before the timer fires, the overlay never
 *      paints. Avoids flash on fast loads.
 *
 * Props (attributes):
 *   active   — overlay visible (default: false)
 *   delay    — grace window in ms before paint (default: 200)
 *   label    — accessible label, also forwarded to auto-stamped spinner
 *   variant  — default | transparent | blur
 *
 * Lifecycle: connected() seeds aria-label on host + caches parent
 * reference for aria-busy bookkeeping. Disconnected() clears any
 * pending delay timer and releases aria-busy on the parent. State
 * changes are driven by the reactive [active] setter wrapper, which
 * starts/cancels the timer + applies aria-busy when the timer fires.
 *
 * @see SPEC-034 (docs/specs/implementation-ready/SPEC-034-loading-overlay.md)
 */

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

export class UILoadingOverlay extends UIElement {
  static properties = {
    active:  { type: Boolean, default: false,      reflect: true },
    delay:   { type: Number,  default: 200,        reflect: true },
    label:   { type: String,  default: 'Loading…', reflect: true },
    variant: { type: String,  default: 'default',  reflect: true },
  };

  // No stamped children authored by the template engine; auto-stamp of
  // the default spinner happens imperatively in connected() / render()
  // when the slot is empty. Authored children (skeleton/progress/etc.)
  // are left in place per ADR-0033 (light-DOM substrate).
  static template = () => null;

  #parent = null;
  #delayTimer = null;
  #activeApplied = false;   // tracks whether aria-busy / opacity actually painted

  constructor() {
    super();
    // Wrap the auto-installed `active` setter so state transitions kick
    // the delay timer synchronously with the assignment — matching the
    // drawer-ui Safari pattern (avoids microtask scheduling delays).
    const desc = Object.getOwnPropertyDescriptor(this, 'active');
    if (desc?.set) {
      const origSet = desc.set;
      Object.defineProperty(this, 'active', {
        get: desc.get,
        set: (v) => {
          const prev = !!desc.get.call(this);
          origSet.call(this, v);
          if (!!v !== prev) this.#syncActive(!!v);
        },
        configurable: true,
      });
    }
  }

  connected() {
    // Host ARIA: role=status is a polite live region; aria-label
    // names the operation. Both are seeded once; render() keeps the
    // label fresh on prop changes.
    if (!this.hasAttribute('role')) this.setAttribute('role', 'status');
    if (!this.hasAttribute('aria-live')) this.setAttribute('aria-live', 'polite');
    this.setAttribute('aria-label', this.label || 'Loading');

    // Cache the parent so disconnected() can release aria-busy even if
    // the element is detached before parentElement is re-readable.
    this.#parent = this.parentElement;

    // If the element mounted with [active] already set (declarative),
    // honor it. The reactive setter wrapper only fires on subsequent
    // assignments, so we kick the timer here too.
    if (this.active) this.#syncActive(true);
  }

  disconnected() {
    // Symmetric lifecycle: clear any in-flight timer + release aria-busy
    // on the parent we cached at connect time.
    this.#clearTimer();
    if (this.#activeApplied) this.#releaseBusy();
    this.#parent = null;
  }

  render() {
    // Keep the host label fresh if [label] is mutated after connect.
    // Forward it to an auto-stamped spinner-ui (if present) so screen
    // readers hear a single coherent name.
    const label = this.label || 'Loading';
    this.setAttribute('aria-label', label);

    const ownSpinner = this.querySelector(':scope > spinner-ui[data-loading-overlay-auto]');
    if (ownSpinner && ownSpinner.getAttribute('label') !== label) {
      ownSpinner.setAttribute('label', label);
    }

    // Ensure the auto-stamped spinner exists when the slot is empty.
    // Authored children (consumer-supplied indicators) win; we only
    // stamp when there are no element children at all.
    if (this.children.length === 0) {
      this.#stampDefaultSpinner();
    }
  }

  // ── Internals ───────────────────────────────────────────────────────

  #syncActive(next) {
    // Clear any prior in-flight timer first; the new state owns the
    // grace window.
    this.#clearTimer();

    if (next) {
      const delay = Math.max(0, Number(this.delay) || 0);
      if (delay === 0) {
        this.#applyBusy();
      } else {
        this.#delayTimer = setTimeout(() => {
          this.#delayTimer = null;
          // Only paint if we're still active — caller may have cleared
          // [active] during the grace window.
          if (this.active) this.#applyBusy();
        }, delay);
      }
    } else if (this.#activeApplied) {
      this.#releaseBusy();
    }
  }

  #applyBusy() {
    const parent = this.#parent || this.parentElement;
    if (parent && !parent.hasAttribute('aria-busy')) {
      parent.setAttribute('aria-busy', 'true');
    }
    this.#activeApplied = true;
  }

  #releaseBusy() {
    const parent = this.#parent || this.parentElement;
    // Only remove aria-busy if WE set it (heuristic: matches "true").
    // If the consumer authored their own aria-busy="true" before we
    // mounted, we leave it; if they set "false" we leave it. Same
    // shape as drawer-ui releasing dialog state.
    if (parent && parent.getAttribute('aria-busy') === 'true') {
      parent.removeAttribute('aria-busy');
    }
    this.#activeApplied = false;
  }

  #clearTimer() {
    if (this.#delayTimer !== null) {
      clearTimeout(this.#delayTimer);
      this.#delayTimer = null;
    }
  }

  #stampDefaultSpinner() {
    // Imperative stamp — light-DOM child, marked with a data attribute
    // so render()'s "did the consumer slot something?" check stays
    // unambiguous on subsequent renders.
    const sp = document.createElement('spinner-ui');
    sp.setAttribute('size', 'lg');
    sp.setAttribute('tone', 'subtle');
    sp.setAttribute('label', this.label || 'Loading');
    sp.setAttribute('data-loading-overlay-auto', '');
    this.appendChild(sp);
  }
}
