/**
 * Non-side-effect class export for `<preview-ui>`.
 *
 * Importing this file gives you the class without auto-registering the tag.
 * The auto-register path is `@adia-ai/web-components/components/preview`.
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <preview-ui> — live code + render, single source of truth.
 *
 * Wrap any authored AdiaUI markup; the component keeps it LIVE in a render
 * stage and shows the SAME markup as escaped, syntax-highlighted, copyable
 * HTML beside it — so a demo's code and its rendered output can never drift
 * (the code IS the render's source).
 *
 *   <preview-ui>
 *     <button-ui text="Save" variant="primary"></button-ui>
 *   </preview-ui>
 *
 * Batteries-included + HTML-first by construction: the author writes one
 * block of HTML, no inline styles, no JS wiring — both panes appear.
 *
 * Attributes:
 *   [layout="split"]  — render + code side-by-side (default: stacked)
 *   [code-first]      — show the code pane above/before the render
 *   [language="…"]    — code-ui language hint (default: html)
 *
 * Light-DOM (ADR-0033): the render pane holds real, live custom elements
 * (re-parsed from the captured source on connect), so interactive demos
 * actually work. `slot=` here is decorative; positioning is by CSS.
 */

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

/** Prepare captured markup for display: strip shared indentation + surrounding
 *  blank lines, and collapse `attr=""` → `attr` (the DOM serializes boolean
 *  attributes like `truncate` / `disabled` with an empty value — show the
 *  cleaner boolean form a consumer would actually type). */
function dedent(src) {
  let lines = src
    .replace(/=""(?=[\s/>])/g, '') // boolean attr: truncate="" → truncate
    // JSON / quote-bearing attribute values serialize as attr="…&quot;…&quot;…" —
    // outerHTML escapes the inner " under the default double-quote delimiter. Re-render
    // them single-quote-delimited (attr='…"…"…') — the clean, valid, copy-paste-able
    // form the author actually wrote (e.g. items='[{"id":"a"}]'). Skipped when the value
    // also contains a ' (no safe single-char delimiter — keep the escaped form).
    .replace(/="([^"]*&quot;[^"]*)"/g, (m, v) => {
      const decoded = v.replace(/&quot;/g, '"').replace(/&amp;/g, '&');
      return decoded.includes("'") ? m : `='${decoded}'`;
    })
    .replace(/\t/g, '  ')
    .split('\n');
  while (lines.length && lines[0].trim() === '') lines.shift();
  while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
  const indentOf = (l) => l.match(/^ */)[0].length;
  // Measure the common indent from the BODY (everything after the first line),
  // not from every line: markup captured via outerHTML/innerHTML puts the
  // opening tag at column 0 while its children + closing tag carry the source
  // file's nesting depth. Including that 0-indent first line would make min=0
  // and leave the whole body deeply indented (the original bug). Clamp each
  // slice to the line's own indent so the 0-indent opener is never truncated.
  const body = lines.slice(1).filter((l) => l.trim());
  const measured = (body.length ? body : lines.filter((l) => l.trim())).map(indentOf);
  const min = measured.length ? Math.min(...measured) : 0;
  return lines.map((l) => l.slice(Math.min(min, indentOf(l)))).join('\n');
}

export class UIPreview extends UIElement {
  static properties = {
    layout:    { type: String,  default: '',     reflect: true },
    codeFirst: { type: Boolean, default: false,  reflect: true, attribute: 'code-first' },
    language:  { type: String,  default: 'html', reflect: true },
    rows:      { type: Boolean, default: false,  reflect: true },
  };

  static template = () => null;

  #fitObserver = null;

  /** A [render-cell | code-cell] pair from one source string. */
  #pane(source, lang, codeFirst) {
    const render = document.createElement('div');
    render.setAttribute('data-preview-render', '');
    render.innerHTML = source; // re-parse → fresh, live custom elements (keeps
                               // any data-chunk corpus markers for harvest)
    const codeWrap = document.createElement('div');
    codeWrap.setAttribute('data-preview-code', '');
    const code = document.createElement('code-ui');
    code.setAttribute('language', lang);
    // The CODE pane shows the clean consumer-facing HTML — strip doc-only
    // corpus markers (data-chunk / -kind / -slot) that the render keeps.
    code.textContent = source.replace(/\s+data-chunk(?:-[a-z]+)?="[^"]*"/g, '');
    codeWrap.appendChild(code);
    return codeFirst ? [codeWrap, render] : [render, codeWrap];
  }

  connected() {
    // Idempotent — never re-stamp (peer mutation / re-connect safe).
    if (this.querySelector(':scope > [data-preview-render], :scope > [data-preview-row]')) return;

    // Side-by-side (render | code) is the default presentation. `rows` mode lays
    // out its own per-example grid, and an explicit layout="…" (e.g. "stack")
    // opts back into the stacked render-over-code form — so only default when
    // neither is set. Setting the attribute (not the property default) keeps
    // rows-mode previews free of a conflicting layout="split".
    if (!this.hasAttribute('rows') && !this.hasAttribute('layout')) {
      this.setAttribute('layout', 'split');
    }

    const lang = this.getAttribute('language') || 'html';
    const codeFirst = this.hasAttribute('code-first');

    // ── rows mode ── each direct child is one example; lay each out as its
    // own [render | code] row so a multi-example gallery reads as paired
    // side-by-side rows instead of all-renders-then-one-code-dump. The code
    // shown is the child's OWN markup (zero drift). An optional
    // [data-preview-label] surfaces a caption per row.
    if (this.hasAttribute('rows')) {
      const children = [...this.children];
      const out = [];
      for (const child of children) {
        const label = child.getAttribute('data-preview-label');
        if (label != null) child.removeAttribute('data-preview-label');
        // A synthetic wrapper (multi-element row grouped by the host) marks
        // itself [data-preview-unwrap] so we show its children's markup, not the
        // throwaway wrapper tag — the copied code stays exactly what the author
        // would paste.
        const unwrap = child.hasAttribute('data-preview-unwrap');
        if (unwrap) child.removeAttribute('data-preview-unwrap');
        const source = dedent(unwrap ? child.innerHTML : child.outerHTML);
        const row = document.createElement('div');
        row.setAttribute('data-preview-row', '');
        const panes = this.#pane(source, lang, codeFirst);
        // The caption pill renders via [data-preview-render]::before, whose
        // attr() can only read its OWN element — so the label goes on the
        // render cell, not the row.
        if (label) {
          const renderEl = panes.find((el) => el.hasAttribute('data-preview-render'));
          renderEl?.setAttribute('data-preview-label', label);
        }
        row.append(...panes);
        out.push(row);
      }
      if (out.length) { this.replaceChildren(...out); this.#observeFit(); return; }
    }

    // ── single-block mode ── the whole slotted markup → one render + one code.
    const source = dedent(this.innerHTML);
    this.replaceChildren(...this.#pane(source, lang, codeFirst));
    this.#observeFit();
  }

  // Side-by-side clips content wider than its half-width cell (a full dashboard
  // composite, a wide control). After layout, if a render cell overflows, fall
  // back to the stacked full-width form so the demo is never cut off. Monotonic
  // (split → stack, never back; a row gains [data-stack] once), so it settles in
  // one pass and never oscillates. Runs on a rAF + a ResizeObserver so it also
  // adapts when the viewport narrows. Self-correcting — no per-component "wide"
  // list to maintain.
  #fit = () => {
    if (this.hasAttribute('rows')) {
      for (const row of this.querySelectorAll(':scope > [data-preview-row]')) {
        const r = row.querySelector(':scope > [data-preview-render]');
        if (r && r.scrollWidth > r.clientWidth + 4) row.setAttribute('data-stack', '');
      }
    } else if (this.getAttribute('layout') === 'split') {
      const r = this.querySelector(':scope > [data-preview-render]');
      if (r && r.scrollWidth > r.clientWidth + 4) this.setAttribute('layout', 'stack');
    }
  };

  #scheduleFit = () => requestAnimationFrame(this.#fit);

  #observeFit() {
    this.#scheduleFit();
    if (typeof ResizeObserver !== 'undefined') {
      this.#fitObserver = new ResizeObserver(this.#scheduleFit);
      this.#fitObserver.observe(this);
    }
  }

  disconnected() {
    this.#fitObserver?.disconnect();
  }
}
