/**
 * HTML → JSX transformer for ReactAppStarter
 *
 * Input: HTML strings produced by the canonical Tailwind adapter
 * (`@specverse/runtime/views/tailwind`). Output: JSX-safe source
 * that can be dropped between JSX tags in a .tsx template.
 *
 * Scope is deliberately narrow:
 *  - Controlled input — only the adapter's output shape matters.
 *  - String transformer with a small tokenizer, not a full HTML parser.
 *  - No SVG / MathML / script / style tags.
 *
 * If the adapter ever produces markup outside this scope, extend the
 * ATTRIBUTE_RENAMES map / VOID_ELEMENTS set + add a focused test,
 * don't broaden the tokenizer into a general HTML parser.
 *
 * See: README.md ("The html-to-jsx transformer — scope").
 */

/**
 * HTML void elements — must be self-closing in JSX.
 */
const VOID_ELEMENTS = new Set([
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
  'input', 'link', 'meta', 'source', 'track', 'wbr',
]);

/**
 * HTML attribute → JSX attribute renames. JSX is case-sensitive and
 * camelCases several HTML attribute names. Only lists the ones the
 * Tailwind adapter actually emits.
 */
const ATTRIBUTE_RENAMES: Record<string, string> = {
  class: 'className',
  for: 'htmlFor',
  tabindex: 'tabIndex',
  readonly: 'readOnly',
  maxlength: 'maxLength',
  minlength: 'minLength',
  colspan: 'colSpan',
  rowspan: 'rowSpan',
  autofocus: 'autoFocus',
  autocomplete: 'autoComplete',
  cellpadding: 'cellPadding',
  cellspacing: 'cellSpacing',
  enctype: 'encType',
  usemap: 'useMap',
  accesskey: 'accessKey',
  contenteditable: 'contentEditable',
  spellcheck: 'spellCheck',
  crossorigin: 'crossOrigin',
  srcset: 'srcSet',
  srcdoc: 'srcDoc',
};

/**
 * Convert an HTML string to JSX-safe source.
 *
 * Idempotent on already-JSX-shaped input.
 */
export function htmlToJsx(html: string): string {
  if (!html) return '';

  const tokens = tokenize(html);
  return tokens.map(transformToken).join('');
}

// ──────────────────────────────────────────────────────────────────────
// Tokenizer
// ──────────────────────────────────────────────────────────────────────

type Token =
  | { kind: 'text'; value: string }
  | { kind: 'tag'; raw: string; tagName: string; isClosing: boolean; isSelfClosed: boolean };

/**
 * Split an HTML string into text and tag tokens. Quoted attribute
 * values never close a tag, even if they contain '>'. The adapter
 * does not emit unbalanced quotes, so a simple state machine is
 * sufficient.
 */
function tokenize(input: string): Token[] {
  const tokens: Token[] = [];
  let i = 0;
  const n = input.length;

  while (i < n) {
    const ch = input[i];
    if (ch === '<') {
      // Start of tag. Consume until the matching '>', respecting quotes.
      const tagStart = i;
      i++; // consume '<'

      let quote: '"' | "'" | null = null;
      while (i < n) {
        const c = input[i];
        if (quote) {
          if (c === quote) quote = null;
          i++;
          continue;
        }
        if (c === '"' || c === "'") {
          quote = c;
          i++;
          continue;
        }
        if (c === '>') {
          i++; // include the '>'
          break;
        }
        i++;
      }

      const raw = input.slice(tagStart, i);
      const isClosing = raw.startsWith('</');
      const isSelfClosed = raw.endsWith('/>');
      const tagName = parseTagName(raw);
      tokens.push({ kind: 'tag', raw, tagName, isClosing, isSelfClosed });
    } else {
      // Text until the next '<'.
      const textStart = i;
      while (i < n && input[i] !== '<') i++;
      tokens.push({ kind: 'text', value: input.slice(textStart, i) });
    }
  }

  return tokens;
}

function parseTagName(tagRaw: string): string {
  // Handles '<foo', '</foo', '<foo ', '<foo/>', '<foo>' etc.
  const m = tagRaw.match(/^<\/?([a-zA-Z][a-zA-Z0-9-]*)/);
  return m ? m[1].toLowerCase() : '';
}

// ──────────────────────────────────────────────────────────────────────
// Transformation
// ──────────────────────────────────────────────────────────────────────

function transformToken(token: Token): string {
  if (token.kind === 'text') {
    // Adapter output never contains raw '{' or '}' in text nodes in
    // the current implementation. If that changes, escape them here
    // via `{'{'}` / `{'}'}`. Flagged for future: a regression test
    // in view-emitter.test.ts that feeds a spec containing literal
    // braces and expects them round-tripped.
    return token.value;
  }

  return transformTag(token.raw, token.tagName, token.isClosing, token.isSelfClosed);
}

/**
 * Transform a single tag: rename attributes, self-close if void.
 *
 * Works inside-out: split into `<`, open-portion, attributes, close-portion, `>`.
 * We only rewrite attribute NAMES (identifier tokens at attribute start),
 * never attribute values or text. The tokenizer guarantees we see the
 * full tag as one unit.
 */
function transformTag(
  raw: string,
  tagName: string,
  isClosing: boolean,
  isSelfClosed: boolean
): string {
  if (isClosing) {
    // '</div>' — nothing to rewrite
    return raw;
  }

  // Split the tag into the tag-name section and the attributes section.
  // Strip the leading '<' and trailing '>' or '/>'.
  const inner = isSelfClosed
    ? raw.slice(1, -2).trimEnd()
    : raw.slice(1, -1);

  // Separate tag name from attributes. The tag name is the first word.
  const nameMatch = inner.match(/^([a-zA-Z][a-zA-Z0-9-]*)(.*)$/s);
  if (!nameMatch) return raw;

  const elementName = nameMatch[1];
  const attrsSection = nameMatch[2];

  // Parse attributes into structured form, rename names, re-serialise.
  // This guarantees we never rewrite anything inside an attribute value.
  const attrs = parseAttributes(attrsSection);
  const renamedAttrs = attrs.map(renameAttribute);
  const rebuiltAttrs = serializeAttributes(renamedAttrs);

  const shouldSelfClose = VOID_ELEMENTS.has(tagName) && !isSelfClosed;

  if (shouldSelfClose || isSelfClosed) {
    return `<${elementName}${rebuiltAttrs} />`;
  }
  return `<${elementName}${rebuiltAttrs}>`;
}

// ──────────────────────────────────────────────────────────────────────
// Attribute tokenizer
// ──────────────────────────────────────────────────────────────────────

interface Attribute {
  name: string;
  /** undefined = boolean attribute, no value. */
  value?: string;
  /** How the original source quoted the value — preserve on output. */
  quote?: '"' | "'";
}

/**
 * Parse the attributes section of a tag (the substring after the tag
 * name, before the closing `>` or `/>`) into a list of attributes.
 * Whitespace is consumed between attributes; values inside quotes are
 * opaque.
 */
function parseAttributes(section: string): Attribute[] {
  const attrs: Attribute[] = [];
  let i = 0;
  const n = section.length;

  while (i < n) {
    // Skip whitespace
    while (i < n && /\s/.test(section[i])) i++;
    if (i >= n) break;

    // Read name (letters, digits, hyphens, colons for namespaced attrs)
    const nameStart = i;
    while (i < n && /[a-zA-Z0-9:_-]/.test(section[i])) i++;
    if (i === nameStart) {
      // Nothing we recognise — advance to avoid infinite loop
      i++;
      continue;
    }
    const name = section.slice(nameStart, i);

    // Look for '=value' or end-of-attr
    // Skip whitespace before '='
    let j = i;
    while (j < n && /\s/.test(section[j])) j++;
    if (j >= n || section[j] !== '=') {
      // Boolean attribute (e.g. `readonly`, `disabled`)
      attrs.push({ name });
      continue;
    }
    // Consume '=' and optional whitespace
    i = j + 1;
    while (i < n && /\s/.test(section[i])) i++;
    if (i >= n) {
      // Malformed: attribute with '=' and no value. Treat as boolean.
      attrs.push({ name });
      break;
    }

    // Read value — quoted or unquoted
    const vc = section[i];
    if (vc === '"' || vc === "'") {
      const quote = vc as '"' | "'";
      i++; // skip opening quote
      const valStart = i;
      while (i < n && section[i] !== quote) i++;
      const value = section.slice(valStart, i);
      if (i < n) i++; // skip closing quote
      attrs.push({ name, value, quote });
    } else {
      // Unquoted value — run until whitespace or '/' or '>'
      const valStart = i;
      while (i < n && !/[\s/>]/.test(section[i])) i++;
      const value = section.slice(valStart, i);
      attrs.push({ name, value });
    }
  }

  return attrs;
}

/** Apply HTML→JSX name rewrite and, for `style`, value conversion. */
function renameAttribute(attr: Attribute): Attribute {
  const lower = attr.name.toLowerCase();
  const newName = ATTRIBUTE_RENAMES[lower] ?? attr.name;

  if (lower === 'style' && attr.value !== undefined) {
    return { name: newName, value: cssStringToJsxObject(attr.value), quote: undefined };
  }

  return { ...attr, name: newName };
}

/**
 * Serialise a list of attributes back into source form. Preserves the
 * original quote style. A value of `{{ ... }}` (already a JSX object
 * expression) is emitted unquoted (no surrounding `"`); it's already
 * valid JSX syntax.
 */
function serializeAttributes(attrs: Attribute[]): string {
  if (attrs.length === 0) return '';
  const parts = attrs.map(attr => {
    if (attr.value === undefined) return ` ${attr.name}`;
    if (attr.value.startsWith('{{') && attr.value.endsWith('}}')) {
      // JSX expression like {{ color: 'red' }} — no quotes.
      return ` ${attr.name}=${attr.value}`;
    }
    const q = attr.quote ?? '"';
    return ` ${attr.name}=${q}${attr.value}${q}`;
  });
  return parts.join('');
}

// ──────────────────────────────────────────────────────────────────────
// CSS → JSX style object
// ──────────────────────────────────────────────────────────────────────

function cssStringToJsxObject(css: string): string {
  const declarations = css
    .split(';')
    .map(d => d.trim())
    .filter(d => d.length > 0);

  if (declarations.length === 0) return '{{}}';

  const entries = declarations
    .map(decl => {
      const colonIdx = decl.indexOf(':');
      if (colonIdx === -1) return null;
      const prop = decl.slice(0, colonIdx).trim();
      const value = decl.slice(colonIdx + 1).trim();
      const camelProp = prop.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
      const escapedValue = value.replace(/'/g, "\\'");
      return `${camelProp}: '${escapedValue}'`;
    })
    .filter((e): e is string => e !== null);

  return `{{ ${entries.join(', ')} }}`;
}
