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

/**
 * <integration-card-ui provider="slack" name="Slack"
 *                       logo="/integrations/slack.svg"
 *                       description="Send AI replies and notifications to channels."
 *                       status="available">
 * </integration-card-ui>
 *
 * Single tile representing one third-party integration. Status drives the
 * button label + variant; tile chrome borrows from <card-ui> but the
 * primitive is its own surface (one tile = one provider, never compose
 * <card-ui> around it).
 *
 * Events:
 *   connect    — { detail: { provider } } fired when `status="available"`
 *   configure  — { detail: { provider } } fired when `status="connected"`
 *   retry      — { detail: { provider } } fired when `status="error"`
 *   disconnect — bubbled from consumer-provided overflow menu items
 *                with `data-action="disconnect"` in the `actions` slot
 */

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

const STATUS = Object.freeze({
  AVAILABLE:    'available',
  CONNECTED:    'connected',
  ERROR:        'error',
  PENDING:      'pending',
  COMING_SOON:  'coming-soon',
});

const ACTION_FOR_STATUS = Object.freeze({
  available:     { event: 'press',     emits: 'connect',    label: 'Connect',    variant: 'primary', icon: '' },
  connected:     { event: 'press',     emits: 'configure',  label: 'Configure',  variant: 'outline', icon: '' },
  error:         { event: 'press',     emits: 'retry',      label: 'Retry',      variant: 'outline', icon: 'arrow-clockwise' },
  pending:       { event: 'press',     emits: '',           label: 'Connecting…', variant: 'outline', icon: '' },
});

const BADGE_FOR_STATUS = Object.freeze({
  connected:    { variant: 'success', text: 'Connected',   icon: 'check' },
  error:        { variant: 'danger',  text: 'Error',       icon: 'warning-circle' },
  pending:      { variant: 'info',    text: 'Connecting…', icon: '' },
  'coming-soon':{ variant: 'muted',   text: 'Coming soon', icon: '' },
});

// Fallback glyph per known provider when no explicit [logo] is supplied, so
// a card set up with just [provider] never renders a blank logo disc. All
// names are verified Phosphor icons. Unknown providers fall back to a generic
// plug glyph.
const PROVIDER_LOGO = Object.freeze({
  slack:   'chat-circle',
  github:  'git-branch',
  gitlab:  'git-branch',
  linear:  'kanban',
  stripe:  'credit-card',
  zapier:  'lightning',
  figma:   'figma-logo',
  notion:  'notion-logo',
  webhook: 'lightning',
  discord: 'discord-logo',
  google:  'google-logo',
});
const DEFAULT_LOGO = 'plug';

export class UIIntegrationCard extends UIElement {
  // Phosphor icons this primitive auto-stamps (without consumer markup).
  // Aggregated by installIconLoadersForRegistered() across all defined
  // elements. Audited by check-required-icons.mjs (slot 11).
  static requiredIcons = ['check', 'warning-circle', 'arrow-clockwise'];

  static properties = {
    provider:        { type: String,  default: '',          reflect: true },
    name:            { type: String,  default: '',          reflect: false },
    logo:            { type: String,  default: '',          reflect: false },
    description:     { type: String,  default: '',          reflect: false },
    status:          { type: String,  default: 'available', reflect: true },
    'error-message': { type: String,  default: '',          attribute: 'error-message' },
    disabled:        { type: Boolean, default: false,       reflect: true },
  };

  static parts = {
    body:      '<div slot="body" data-integration-card-body></div>',
    logo:      '<div slot="logo" data-integration-card-logo></div>',
    heading:   '<div slot="heading" data-integration-card-heading></div>',
    desc:      '<p slot="description" data-integration-card-description></p>',
    error:     '<p slot="error" data-integration-card-error></p>',
    status:    '<badge-ui slot="status" data-integration-card-status></badge-ui>',
    footer:    '<footer slot="footer" data-integration-card-footer></footer>',
    button:    '<button-ui slot="action" data-integration-card-action></button-ui>',
  };

  static template = () => null;

  // Stable ID for aria-labelledby pointing at the provider name.
  #headingId = '';

  // Cached element refs (lazily filled in `connected()`).
  #bodyEl = null;
  #logoEl = null;
  #headingEl = null;
  #descEl = null;
  #errorEl = null;
  #statusEl = null;
  #footerEl = null;
  #buttonEl = null;

  connected() {
    // Group semantic — labeled region announced by SR.
    this.setAttribute('role', 'group');

    // Build the static frame once. ensure() is idempotent.
    this.#bodyEl    = this.ensure('body');
    this.#logoEl    = this.ensure('logo');
    this.#headingEl = this.ensure('heading');
    this.#descEl    = this.ensure('desc');
    this.#statusEl  = this.ensure('status');
    this.#footerEl  = this.ensure('footer');

    // Slot/structural arrangement: logo + heading + status sit inside body row;
    // description sits below; error sits below description; button sits in footer.
    // We arrange via DOM order so CSS direct-child selectors apply consistently.
    if (this.#logoEl.parentElement !== this.#bodyEl)    this.#bodyEl.appendChild(this.#logoEl);
    if (this.#headingEl.parentElement !== this.#bodyEl) this.#bodyEl.appendChild(this.#headingEl);
    if (this.#statusEl.parentElement !== this.#bodyEl)  this.#bodyEl.appendChild(this.#statusEl);

    // Assign a stable id for aria-labelledby (don't clobber author-set id).
    if (!this.#headingEl.id) {
      this.#headingId = `integration-card-heading-${Math.random().toString(36).slice(2, 9)}`;
      this.#headingEl.id = this.#headingId;
    } else {
      this.#headingId = this.#headingEl.id;
    }
    this.setAttribute('aria-labelledby', this.#headingId);

    // Wire action button activation. The button-ui dispatches `press`; we
    // re-dispatch a typed event (connect / configure / retry) based on status.
    this.addEventListener('press', this.#onPress);

    // Catch consumer-wired overflow actions in the `actions` slot. The
    // menu-ui dispatches `action` with the action name in detail — when
    // it's `disconnect`, we re-dispatch as a typed event with the
    // provider key. Same recipe for `reauth` / `re-authenticate`.
    this.addEventListener('action', this.#onMenuAction);
  }

  disconnected() {
    this.removeEventListener('press', this.#onPress);
    this.removeEventListener('action', this.#onMenuAction);
    this.#bodyEl = null;
    this.#logoEl = null;
    this.#headingEl = null;
    this.#descEl = null;
    this.#errorEl = null;
    this.#statusEl = null;
    this.#footerEl = null;
    this.#buttonEl = null;
  }

  render() {
    if (!this.#headingEl) return;

    const status = this.#normalizedStatus();

    // ── Heading ──────────────────────────────────────────────────────
    if (this.name && this.#headingEl.textContent !== this.name) {
      this.#headingEl.textContent = this.name;
    }

    // ── Description ─────────────────────────────────────────────────
    // The default slot supersedes the description prop when non-empty.
    // We detect "consumer-authored description" via any direct child that
    // isn't one of our stamped data-integration-card-* elements and isn't
    // a slotted action / menu.
    const hasCustomDesc = this.#hasConsumerDescription();
    if (hasCustomDesc) {
      if (this.#descEl.parentElement === this) {
        this.#descEl.remove();
      }
    } else {
      if (!this.#descEl.parentElement) this.appendChild(this.#descEl);
      if (this.description) {
        if (this.#descEl.textContent !== this.description) {
          this.#descEl.textContent = this.description;
        }
        this.#descEl.hidden = false;
      } else {
        this.#descEl.textContent = '';
        this.#descEl.hidden = true;
      }
    }

    // ── Logo ────────────────────────────────────────────────────────
    this.#renderLogo();

    // ── Status badge ────────────────────────────────────────────────
    this.#renderStatusBadge(status);

    // ── Error message (status="error" only) ─────────────────────────
    this.#renderError(status);

    // ── Action button (label + variant + visibility) ────────────────
    this.#renderActionButton(status);
  }

  // ── Private rendering helpers ─────────────────────────────────────

  #normalizedStatus() {
    const s = (this.status || '').trim() || STATUS.AVAILABLE;
    // Validate against the enum; fall back to `available` on bad input.
    if (Object.values(STATUS).includes(s)) return s;
    return STATUS.AVAILABLE;
  }

  #hasConsumerDescription() {
    // Author description = direct child element that is neither one of our
    // stamped data- markers nor an [slot="actions"] / [slot="action"] node.
    // wrapper-trap-ok: detects presence of consumer-provided description; wrapper spans from interpolated content also qualify as "consumer description present" (correct — the content IS there)
    for (const child of this.children) {
      if (child === this.#bodyEl) continue;
      if (child === this.#descEl) continue;
      if (child === this.#errorEl) continue;
      if (child === this.#footerEl) continue;
      if (child.getAttribute && child.getAttribute('slot') === 'actions') continue;
      if (child.dataset && (
        child.dataset.integrationCardBody !== undefined ||
        child.dataset.integrationCardDescription !== undefined ||
        child.dataset.integrationCardError !== undefined ||
        child.dataset.integrationCardFooter !== undefined
      )) continue;
      // Any other direct child counts as consumer-authored description content.
      return true;
    }
    return false;
  }

  #renderLogo() {
    if (!this.#logoEl) return;
    const logo = (this.logo || '').trim();

    // Guard against unresolved AdiaUI template binding descriptors
    // ({{p:N}} placeholders). The integration-card's connected() + render()
    // fires synchronously when the parent template stamps the element, BEFORE
    // the parent's reconciliation pass replaces {{p:N}} with the real value.
    // Skipping here lets the second render (triggered by the reconciliation's
    // property set) render the real logo. Without this guard, icon-ui fires
    // a "not found" warn for every {{p:4}} it receives on first connect.
    if (logo.startsWith('{{p:')) return;

    // Resolve the visual: an explicit [logo] wins; otherwise fall back to a
    // per-provider glyph (or a generic plug) so a card with only [provider]
    // never renders a blank disc. Hide only when there's neither.
    const provider = (this.provider || '').trim().toLowerCase();
    const resolved = logo || PROVIDER_LOGO[provider] || (provider ? DEFAULT_LOGO : '');

    if (!resolved) {
      this.#logoEl.replaceChildren();
      this.#logoEl.hidden = true;
      return;
    }
    this.#logoEl.hidden = false;

    // URL vs icon-name sniff: presence of '/' → URL.
    const isUrl = resolved.includes('/');

    if (isUrl) {
      // Reuse existing <img> if same src; otherwise re-stamp.
      let img = this.#logoEl.querySelector(':scope > img');
      if (img && img.getAttribute('src') === resolved) {
        img.setAttribute('alt', `${this.name || this.provider || 'Integration'} logo`);
        return;
      }
      this.#logoEl.replaceChildren();
      img = document.createElement('img');
      img.setAttribute('src', resolved);
      img.setAttribute('alt', `${this.name || this.provider || 'Integration'} logo`);
      img.setAttribute('data-integration-logo', '');
      img.setAttribute('loading', 'lazy');
      img.setAttribute('decoding', 'async');
      this.#logoEl.appendChild(img);
      return;
    }

    // Icon name → <icon-ui>.
    let icon = this.#logoEl.querySelector(':scope > icon-ui');
    if (icon && icon.getAttribute('name') === resolved) return;
    this.#logoEl.replaceChildren();
    icon = document.createElement('icon-ui');
    icon.setAttribute('name', resolved);
    icon.setAttribute('aria-hidden', 'true');
    this.#logoEl.appendChild(icon);
  }

  #renderStatusBadge(status) {
    if (!this.#statusEl) return;
    const cfg = BADGE_FOR_STATUS[status];
    if (!cfg) {
      // No badge for `available` — keep DOM but hide.
      this.#statusEl.hidden = true;
      this.#statusEl.removeAttribute('aria-label');
      return;
    }
    this.#statusEl.hidden = false;
    this.#statusEl.setAttribute('variant', cfg.variant);
    this.#statusEl.setAttribute('text', cfg.text);
    this.#statusEl.setAttribute('size', 'sm');
    if (cfg.icon) this.#statusEl.setAttribute('icon', cfg.icon);
    else this.#statusEl.removeAttribute('icon');
    // Explicit aria-label so SR announces "Connected" even when text rendering
    // is delayed by icon-registry boot.
    this.#statusEl.setAttribute('aria-label', cfg.text);
  }

  #renderError(status) {
    const msg = (this['error-message'] || this.getAttribute('error-message') || '').trim();
    if (status === STATUS.ERROR && msg) {
      if (!this.#errorEl) this.#errorEl = this.ensure('error');
      if (!this.#errorEl.parentElement) {
        // Insert after description, before footer if possible.
        if (this.#descEl?.parentElement === this) {
          this.#descEl.after(this.#errorEl);
        } else if (this.#footerEl?.parentElement === this) {
          this.insertBefore(this.#errorEl, this.#footerEl);
        } else {
          this.appendChild(this.#errorEl);
        }
      }
      if (this.#errorEl.textContent !== msg) this.#errorEl.textContent = msg;
      this.#errorEl.hidden = false;
      this.#errorEl.setAttribute('role', 'status');
    } else if (this.#errorEl) {
      this.#errorEl.hidden = true;
      this.#errorEl.textContent = '';
    }
  }

  #renderActionButton(status) {
    // coming-soon → hide the button entirely; the badge carries the label.
    if (status === STATUS.COMING_SOON) {
      this.drop('button');
      this.#buttonEl = null;
      // Footer with no button can still hold the [slot="actions"] menu,
      // so we keep the footer node attached.
      return;
    }

    const cfg = ACTION_FOR_STATUS[status];
    if (!cfg) {
      this.drop('button');
      this.#buttonEl = null;
      return;
    }

    // Ensure the button exists and sits inside the footer (light DOM
    // positioning: button is a direct child of footer, footer is a direct
    // child of host).
    if (!this.#buttonEl) this.#buttonEl = this.ensure('button');
    if (this.#buttonEl.parentElement !== this.#footerEl) {
      this.#footerEl.insertBefore(this.#buttonEl, this.#footerEl.firstChild);
    }

    this.#buttonEl.setAttribute('text', cfg.label);
    this.#buttonEl.setAttribute('variant', cfg.variant);
    this.#buttonEl.setAttribute('size', 'sm');
    if (cfg.icon) this.#buttonEl.setAttribute('icon', cfg.icon);
    else this.#buttonEl.removeAttribute('icon');

    // Toggle semantics — aria-pressed reflects "is the integration on?"
    // Provides screen-reader feedback on the action's effect.
    if (status === STATUS.CONNECTED) {
      this.#buttonEl.setAttribute('aria-pressed', 'true');
    } else if (status === STATUS.AVAILABLE) {
      this.#buttonEl.setAttribute('aria-pressed', 'false');
    } else {
      this.#buttonEl.removeAttribute('aria-pressed');
    }

    // Explicit aria-label so SR announces "Connect to Slack" outside group ctx.
    const verbForStatus = {
      available:  'Connect to',
      connected:  'Configure',
      error:      'Retry connecting',
      pending:    'Connecting to',
    }[status] || cfg.label;
    const labelTarget = this.name || this.provider || 'integration';
    this.#buttonEl.setAttribute('aria-label', `${verbForStatus} ${labelTarget}`);

    // pending → non-interactive; disabled prop → non-interactive too.
    if (status === STATUS.PENDING || this.disabled) {
      this.#buttonEl.setAttribute('disabled', '');
    } else {
      this.#buttonEl.removeAttribute('disabled');
    }
  }

  // ── Event handlers ────────────────────────────────────────────────

  #onPress = (e) => {
    // Only react to presses on OUR action button — overflow menu items
    // dispatch `action` instead.
    if (!this.#buttonEl) return;
    if (e.target !== this.#buttonEl && !e.composedPath?.().includes(this.#buttonEl)) return;
    if (this.disabled) { e.stopPropagation(); return; }

    const status = this.#normalizedStatus();
    if (status === STATUS.PENDING || status === STATUS.COMING_SOON) {
      e.stopPropagation();
      return;
    }

    const cfg = ACTION_FOR_STATUS[status];
    if (!cfg || !cfg.emits) return;

    this.dispatchEvent(new CustomEvent(cfg.emits, {
      bubbles: true,
      detail: { provider: this.provider },
    }));
  };

  #onMenuAction = (e) => {
    // Bubbled `action` events from the consumer-provided overflow menu
    // in [slot="actions"]. Detail shape from menu-item-ui: { action: '<name>' }.
    const action = e.detail?.action || e.target?.getAttribute?.('action');
    if (!action) return;
    if (action === 'disconnect' || action === 'reauth' || action === 're-authenticate') {
      const emits = action === 'disconnect' ? 'disconnect' : 'reauth';
      this.dispatchEvent(new CustomEvent(emits, {
        bubbles: true,
        detail: { provider: this.provider },
      }));
    }
  };
}
