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

/**
 * <inline-message-ui text="Email address is required" variant="danger"></inline-message-ui>
 * <inline-message-ui text="At least 8 characters"></inline-message-ui>          ← default hint
 * <inline-message-ui variant="success" live="polite" text="Saved"></inline-message-ui>
 *
 * In-flow message glyph + text for form-field rows. Distinguished from
 * <alert-ui>: no surface fill, no border, no padding box — severity is
 * foreground color + icon only.
 *
 * Variants: default (muted, no icon) | info | success | warning | danger
 * Slots:    leading (icon — auto-stamped from variant), default (text body)
 *
 * The host carries no role of its own (transparent inline annotation).
 * Screen-reader pickup happens through the surrounding <field-ui>'s
 * `aria-describedby` wiring. For dynamic message updates, opt into
 * a live region with [live="polite"] or [live="assertive"].
 */

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

// variant → default icon name. Pure default carries no icon (muted hint).
const VARIANT_ICON = {
  info:    'info',
  success: 'check-circle',
  warning: 'warning',
  danger:  'x-circle',
};

export class UIInlineMessage extends UIElement {
  static properties = {
    text:    { type: String, default: '',        reflect: false },
    variant: { type: String, default: 'default', reflect: true },
    icon:    { type: String, default: '',        reflect: true },
    live:    { type: String, default: '',        reflect: true },
  };

  // Static parts — single `parts.leading` for the auto-stamped icon
  // (per SPEC-033 §8). render() re-runs ensure/drop based on current
  // variant + icon + the absence of consumer-provided slotted icon.
  static parts = {
    leading: '<icon-ui slot="leading"></icon-ui>',
  };

  static requiredIcons = ['info', 'check-circle', 'warning', 'x-circle'];

  static template = () => null;

  /**
   * Was the current leading icon consumer-provided (preserve across
   * variant changes) or auto-stamped by us (re-stamp on variant change)?
   * Detected by the `_uiPart` marker that `ensure()` sets on stamped
   * parts plus our own `data-im-auto` tag so a consumer-replaced node
   * doesn't get clobbered. See `ensure()` in core/element.js.
   */
  #ownsLeading(node) {
    return !!(node && (node._uiPart || node.hasAttribute('data-im-auto')));
  }

  connected() {
    // Stamp leading icon if variant supplies one and consumer hasn't
    // already slotted their own. render() will mirror this on changes.
  }

  render() {
    // ── aria-live wiring ──
    // [live=""] → strip aria-live. [live=polite|assertive] → reflect.
    if (this.live) {
      this.setAttribute('aria-live', this.live);
    } else if (this.hasAttribute('aria-live')) {
      this.removeAttribute('aria-live');
    }

    // ── default-slot text body ──
    // If [text] is set and the consumer hasn't slotted body content,
    // mirror [text] into textContent of the host so CSS lays it out
    // alongside the leading icon. Detection of "consumer slotted body":
    // any non-slotted child text/element survives across renders.
    //
    // NOTE: we never overwrite consumer-provided body content. We tag
    // our own text node so we can replace it on subsequent renders
    // without touching the consumer's nodes.
    this.#syncBodyText();

    // ── leading icon stamping ──
    // Resolve which icon to show: explicit [icon] wins, then the
    // variant's default. Default variant has no icon (pure hint).
    const resolvedIcon = this.icon || VARIANT_ICON[this.variant] || '';

    // Inspect the current `[slot="leading"]` child. If consumer-provided
    // (no _uiPart flag, no data-im-auto), leave it untouched — they
    // own it across variant changes.
    const existingLeading = this.querySelector(':scope > [slot="leading"]');
    if (existingLeading && !this.#ownsLeading(existingLeading)) {
      // Consumer owns the leading slot; do nothing.
      return;
    }

    if (resolvedIcon) {
      const leading = this.ensure('leading');
      if (leading) {
        // Mark as auto-stamped so subsequent renders / variant changes
        // re-stamp without prompting "is this consumer-owned?" ambiguity.
        leading.setAttribute('data-im-auto', '');
        if (leading.getAttribute('name') !== resolvedIcon) {
          leading.setAttribute('name', resolvedIcon);
        }
      }
    } else {
      this.drop('leading');
    }
  }

  /**
   * Mirror [text] into the host body without clobbering consumer-slotted
   * nodes. We anchor our text node with a `data-im-text` marker on a
   * `<span>` wrapper; on subsequent renders we update that span's
   * textContent. Any non-marked child (including the leading slot) is
   * left untouched.
   */
  #syncBodyText() {
    // Find our auto-stamped text span, if any.
    let bodySpan = this.querySelector(':scope > [data-im-text]');

    // Detect any consumer-provided body content: child nodes that
    // are NOT the leading slot and NOT our text span. If present,
    // we drop our text span entirely (consumer wins).
    const hasConsumerBody = Array.from(this.childNodes).some((n) => {
      if (n === bodySpan) return false;
      if (n.nodeType === Node.ELEMENT_NODE) {
        if (n.getAttribute && n.getAttribute('slot') === 'leading') return false;
        return true;
      }
      // Non-empty text node from author markup
      if (n.nodeType === Node.TEXT_NODE && n.textContent.trim()) return true;
      return false;
    });

    if (hasConsumerBody) {
      if (bodySpan) bodySpan.remove();
      return;
    }

    if (this.text) {
      if (!bodySpan) {
        bodySpan = document.createElement('span');
        bodySpan.setAttribute('data-im-text', '');
        this.appendChild(bodySpan);
      }
      if (bodySpan.textContent !== this.text) {
        bodySpan.textContent = this.text;
      }
    } else if (bodySpan) {
      bodySpan.remove();
    }
  }
}
