import type { EntityRef, ComposerDoc } from '../primitives/composer-model';
import { normalizeValue } from '../primitives/composer-model';

export const ZWSP = '​';
export const ENTITY_ATTR = 'data-kai-entity';
export const entityStore = new WeakMap<HTMLElement, EntityRef>();

export function isEntityEl(node: Node | null): node is HTMLElement {
  return !!node && node.nodeType === 1 && (node as HTMLElement).hasAttribute(ENTITY_ATTR);
}

/**
 * A TreeWalker over the editable's text nodes that SKIPS text inside entity
 * pills. Pills are atomic and contribute nothing to the text model — their inner
 * label text must not pollute caret offsets, trigger detection, or highlight
 * ranges. (Without this, the text seen at a caret right after a pill is the
 * pill's label, so `/` reads as glued to the label instead of starting a token.)
 */
export function createTextWalker(root: HTMLElement): TreeWalker {
  return root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
    acceptNode(node) {
      let p = node.parentElement;
      while (p && p !== root) {
        if (p.hasAttribute(ENTITY_ATTR)) return NodeFilter.FILTER_REJECT;
        p = p.parentElement;
      }
      return NodeFilter.FILTER_ACCEPT;
    },
  });
}

/**
 * The leading sigil shown on a "light" pill, by kind: skills `/`, agents `@`.
 * Skills and agents render as decorated inline text led by their sigil; this is
 * what makes them read like `/my-skill` / `@my-agent` rather than a chip.
 * Returns '' for kinds rendered as a richer CHIP (plugins, and any other/unknown
 * kind) — those carry an icon instead of a sigil. The sigil is visual only: it
 * never enters the text model (the entity is read from `entityStore`, and the
 * text walker skips pill-internal nodes).
 */
export function kindSigil(kind: string): string {
  switch (kind) {
    case 'skill':
      return '/';
    case 'agent':
      return '@';
    default:
      return '';
  }
}

/**
 * Default monochrome (currentColor) glyph per entity kind, for the richer CHIP
 * kinds shown in the trigger menu / on plugin pills: plugin = plug. (Skills and
 * agents are light sigil-text pills — see `kindSigil` — so they take no glyph on
 * the pill, though the menu may still show one.) Returns inline SVG markup
 * (trusted — no user input), or '' for kinds without a default. An item's own
 * `icon` always takes precedence over this.
 */
export function kindGlyph(kind: string): string {
  const svg = (inner: string) =>
    `<svg viewBox="0 0 24 24" width="1em" height="1em" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
  switch (kind) {
    case 'agent':
      return svg('<rect x="4" y="8" width="16" height="12" rx="3"/><path d="M12 4.5V8M9.5 13.5v1.5M14.5 13.5v1.5"/><circle cx="12" cy="3" r="1.2"/>');
    case 'plugin':
      return svg('<path d="M9 2v4M15 2v4M6 6h12v5a6 6 0 0 1-12 0zM12 17v5"/>');
    default:
      return ''; // skills + unknown kinds: no default glyph
  }
}

export function createEntityEl(
  doc: Document,
  entity: EntityRef,
  kindIcons?: Record<string, string>,
): HTMLElement {
  const el = doc.createElement('span');
  el.setAttribute(ENTITY_ATTR, '');
  el.setAttribute('contenteditable', 'false');
  el.dataset.kind = entity.kind;
  el.dataset.id = entity.id;
  el.className = 'kai-composer-pill';

  const sigil = kindSigil(entity.kind);
  if (sigil) {
    // Skills/agents: LIGHT pill — decorated inline text led by the sigil, no
    // icon. (`/my-skill`, `@my-agent`.)
    const s = doc.createElement('span');
    s.className = 'kai-composer-pill-sigil';
    s.setAttribute('aria-hidden', 'true');
    s.textContent = sigil;
    el.appendChild(s);
  } else {
    // Plugins (and other composite/unknown kinds): the richer CHIP, with an
    // icon so a bundle reads differently from a skill/agent at a glance. Icon
    // resolution: the item's own icon → per-kind default (kindIcons) → a
    // built-in kind glyph (plugin) → nothing.
    const iconSrc = entity.icon ?? kindIcons?.[entity.kind];
    if (iconSrc) {
      const img = doc.createElement('img');
      img.src = iconSrc;
      img.alt = '';
      img.className = 'kai-composer-pill-icon';
      el.appendChild(img);
    } else {
      const glyph = kindGlyph(entity.kind);
      if (glyph) {
        const span = doc.createElement('span');
        span.className = 'kai-composer-pill-icon kai-composer-pill-glyph';
        span.setAttribute('aria-hidden', 'true');
        span.innerHTML = glyph; // trusted SVG markup (not user input)
        el.appendChild(span);
      }
    }
  }
  el.appendChild(doc.createTextNode(entity.label));
  entityStore.set(el, entity);
  return el;
}

export function parseDom(root: HTMLElement): ComposerDoc {
  const segs: ComposerDoc = [];
  const nodes = root.childNodes;
  nodes.forEach((node, i) => {
    if (isEntityEl(node)) {
      const stored = entityStore.get(node as HTMLElement);
      const el = node as HTMLElement;
      segs.push({ type: 'entity', entity: stored ?? { kind: el.dataset.kind ?? '', id: el.dataset.id ?? '', label: el.textContent ?? '' } });
    } else if (node.nodeType === 1 && (node as HTMLElement).tagName === 'BR') {
      // Drop the browser's trailing filler <br> — contenteditable inserts one when
      // the field is cleared (or after a newline) to keep the line visible. Treating
      // it as content would leave the field "non-empty" (placeholder stuck hidden)
      // and serialize a phantom trailing "\n". A <br> BETWEEN content is a real
      // newline and is preserved.
      if (i === nodes.length - 1) return;
      segs.push({ type: 'text', text: '\n' });
    } else if (node.nodeType === 3) {
      segs.push({ type: 'text', text: (node.textContent ?? '').split(ZWSP).join('') });
    }
  });
  return normalizeValue(segs);
}

export function renderDoc(
  root: HTMLElement,
  doc: ComposerDoc,
  ownerDoc: Document = document,
  kindIcons?: Record<string, string>,
): void {
  root.textContent = '';
  for (const seg of doc) {
    if (seg.type === 'text') root.appendChild(ownerDoc.createTextNode(seg.text));
    else {
      root.appendChild(createEntityEl(ownerDoc, seg.entity, kindIcons));
      root.appendChild(ownerDoc.createTextNode(ZWSP));
    }
  }
}
