/**
 * `UIButton` class — non-side-effect export.
 *
 * Importing this file gives you the class without auto-registering the
 * tag. Useful for:
 *   - Test isolation (register under a different tag in a test)
 *   - Subclassing with tag-name override
 *   - Tree-shaking-conscious bundles that compose primitives selectively
 *
 * The auto-register path stays at `@adia-ai/web-components/components/button`
 * (which imports this file + calls `customElements.define`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

import { UIElement, signal, html } from '../../core/element.js';
import { getIcon } from '../../core/icons.js';

// FEEDBACK-15: highest-frequency icon ⇄ text-prefix collisions. When an
// author writes e.g. text="+ New Item" icon="plus", the Phosphor glyph and
// the literal symbol both render — a doubled "+". Map is intentionally a
// curated high-frequency set, not exhaustive; the warning is non-breaking.
const ICON_TEXT_PREFIXES = {
  plus:          ['+', '＋'],
  minus:         ['-', '−', '–'],
  x:             ['×', 'x ', 'X '],
  check:         ['✓', '✔'],
  'arrow-right': ['→', '>'],
  'arrow-left':  ['←', '<'],
};

export class UIButton extends UIElement {
  static properties = {
    text:     { type: String,  default: '', reflect: true },
    // NOTE: `textContent` is intentionally NOT declared as a reactive
    // prop — `installProps()` would override the native DOM setter, so
    // `el.textContent = '…'` would silently no-op instead of updating
    // child text. Yaml keeps the prop entry for renderer metadata, but
    // the JS class must not declare it. v0.5.x §327.
    variant:  { type: String,  default: 'solid', reflect: true },
    color:    { type: String,  default: '', reflect: true },
    size:     { type: String,  default: 'md', reflect: true },
    disabled: { type: Boolean, default: false, reflect: true },
    stretch:  { type: Boolean, default: false, reflect: true },
    icon:     { type: String,  default: '', reflect: true },
    iconTrailing: { type: String, default: '', reflect: true, attribute: 'icon-trailing' },
    type:     { type: String,  default: 'button', reflect: true },
  };

  static template = () => null;

  connected() {
    this.setAttribute('role', 'button');
    this.setAttribute('tabindex', '0');
    this.addEventListener('click', this.#onClick);
    this.addEventListener('keydown', this.#onKey);
  }

  render() {
    // Don't clobber a user-provided aria-label with an empty string when
    // text is unset (e.g. icon-only button with author-set aria-label).
    // Only auto-set when we have meaningful text to put there.
    if (this.text) this.setAttribute('aria-label', this.text);
    if (this.icon) {
      const existing = this.querySelector(':scope > icon-ui:not([slot])');
      if (!existing || existing.name !== this.icon) {
        if (existing) existing.remove();
        const iconEl = document.createElement('icon-ui');
        iconEl.setAttribute('name', this.icon);
        this.prepend(iconEl);
      }
    }

    if (this.iconTrailing) {
      const existing = this.querySelector(':scope > icon-ui[slot="icon-trailing"]');
      if (!existing || existing.name !== this.iconTrailing) {
        if (existing) existing.remove();
        const iconEl = document.createElement('icon-ui');
        iconEl.setAttribute('name', this.iconTrailing);
        iconEl.setAttribute('slot', 'icon-trailing');
        this.appendChild(iconEl);
      }
    }

    // FEEDBACK-15: warn when text= begins with a symbol that icon= already
    // renders (e.g. text="+ New Claim" icon="plus" → a doubled "+"). One-shot
    // per element via WeakSet; warning-only — the button still renders. The
    // text value is never mutated (text="+1" with no icon is a valid label).
    if (this.icon && this.text) {
      const prefixes = ICON_TEXT_PREFIXES[this.icon];
      if (prefixes && prefixes.some((p) => this.text.startsWith(p))) {
        if (!UIButton.#dupSymbolWarned.has(this)) {
          UIButton.#dupSymbolWarned.add(this);
          // eslint-disable-next-line no-console
          console.warn(
            `[button-ui] text="${this.text}" begins with a symbol that ` +
            `icon="${this.icon}" already renders — the glyph appears twice. ` +
            `Drop the leading symbol from text= (the icon provides it).`,
            this,
          );
        }
      }
    }

    // §184 (v0.5.5, FEEDBACK-08 §8): icon-only a11y warning + title→aria-label
    // auto-derive. Two complementary mechanisms (consumer chooses):
    //   (a) When [title="Undo"] is set on an icon-only button without an
    //       explicit aria-label, mirror title → aria-label. Matches the
    //       common icon-only-button-with-tooltip pattern (zero authoring
    //       cost — already had to write the tooltip text).
    //   (b) When the button is icon-only AND has no aria-label AND no
    //       aria-labelledby AND no title, console.warn ONCE per element
    //       so the bug class is diagnosable in dev without firing every
    //       render tick. The warning is suppressed via a one-shot
    //       WeakSet so subsequent re-renders stay quiet.
    if (this.icon && !this.text) {
      const ariaLabel = this.getAttribute('aria-label');
      const ariaLabelledBy = this.getAttribute('aria-labelledby');
      const titleAttr = this.getAttribute('title');
      if (!ariaLabel && !ariaLabelledBy && titleAttr) {
        this.setAttribute('aria-label', titleAttr);
      } else if (!ariaLabel && !ariaLabelledBy && !titleAttr) {
        if (!UIButton.#a11yWarned.has(this)) {
          UIButton.#a11yWarned.add(this);
          // eslint-disable-next-line no-console
          console.warn(
            `[button-ui] Icon-only button is missing an accessible name.\n` +
            `  Element: <button-ui icon="${this.icon}">\n` +
            `  Add one of:\n` +
            `    aria-label="Undo"        ← explicit accessible name\n` +
            `    title="Undo"             ← auto-mirrored to aria-label + tooltip\n` +
            `    aria-labelledby="..."    ← reference an existing label element\n` +
            `  Without an accessible name screen readers announce nothing meaningful.`
          );
        }
      }
    }
  }

  // §184: WeakSet of elements we've already warned about. Prevents
  // repeat warnings across re-renders. GC-friendly — when the element
  // is gone the entry is collected.
  static #a11yWarned = new WeakSet();

  // FEEDBACK-15: one-shot set for the text/icon symbol-duplication warning.
  static #dupSymbolWarned = new WeakSet();

  #onClick = (e) => {
    if (this.disabled) { e.stopPropagation(); return; }
    if (this.type === 'submit') {
      const form = this.closest('form');
      if (form?.requestSubmit) form.requestSubmit();
    } else if (this.type === 'reset') {
      const form = this.closest('form');
      if (form?.reset) form.reset();
    }
    this.dispatchEvent(new Event('press', { bubbles: true }));
  };

  #onKey = (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      this.click();
    }
  };

  disconnected() {
    this.removeEventListener('click', this.#onClick);
    this.removeEventListener('keydown', this.#onKey);
  }
}
