/**
 * JSX-source emitter for Factory B
 *
 * Consumes the pattern-walker's RenderNode tree and produces JSX
 * text that can be written into a .tsx file. Pluggable into
 * Factory B's per-model per-view emission; the composers are being
 * migrated one view type at a time to use this path.
 *
 * Emitter is deliberately minimal:
 * - Atomic nodes whose component is a lowercase HTML tag
 *   (div, table, tr, td, ...) → literal JSX element with props.
 * - Named composite nodes (capitalised — DataTable, FieldList, ...)
 *   → treated as React component references; the emitter collects
 *   them into the imports set so view-emitter can add the
 *   imports at the top of the file.
 * - Special pseudo-components (__text__, __when_empty__, __todo__)
 *   → each handled inline.
 *
 * Round-trip format: the output is readable, indented JSX. Not
 * prettier-perfect but close.
 */

import type { RenderNode } from '@specverse/runtime/views/core';

export interface EmitOptions {
  /** Set of component names referenced (composite components only,
   *  not HTML tags). View-emitter injects these as imports at the
   *  top of the generated .tsx file. */
  imports: Set<string>;
  /** Initial indent — defaults to 6 (matches the {{BODY}} placement
   *  inside the skeleton's <form>/<div> wrapper). */
  indent?: number;
}

/**
 * Emit a tree of RenderNodes to JSX source. Returns the JSX text;
 * composite component names used are written into `options.imports`.
 */
export function emitJsxSource(
  nodes: RenderNode[],
  options: EmitOptions,
): string {
  const indent = options.indent ?? 0;
  return nodes.map(n => emitNode(n, indent, options.imports)).join('\n');
}

/**
 * Width past which we break the attr list onto its own lines.
 * Keeps short elements compact (`<th className="...">Name</th>`)
 * while long attribute lists (an input with required + value +
 * onChange + ...) go multi-line for readability.
 */
const ATTR_BREAK_WIDTH = 80;

function emitNode(node: RenderNode, indent: number, imports: Set<string>): string {
  const pad = ' '.repeat(indent);

  // Pseudo-components — intercepted before the generic path.
  if (node.component === '__text__') {
    return `${pad}${(node.props?.text as string) ?? ''}`;
  }
  if (node.component === '__todo__') {
    const msg = (node.props?.message as string) ?? 'TODO';
    return `${pad}{/* TODO: ${msg} */}`;
  }
  if (node.component === '__when_empty__') {
    const source = (node.props?.source as string) ?? 'items';
    const inner = (node.children ?? [])
      .map(c => emitNode(c, indent + 2, imports))
      .join('\n');
    return [
      `${pad}{${source}.length === 0 && (`,
      inner,
      `${pad})}`,
    ].join('\n');
  }
  if (node.component === '__when__') {
    // Generic conditional render — wraps children in
    // `{condition && (...)}`. Used by detail-view tabbed lists where
    // each panel renders only if `relatedTab === 'X'`.
    const condition = (node.props?.condition as string) ?? 'true';
    const inner = (node.children ?? [])
      .map(c => emitNode(c, indent + 2, imports))
      .join('\n');
    return [
      `${pad}{${condition} && (`,
      inner,
      `${pad})}`,
    ].join('\n');
  }

  const isComposite = /^[A-Z]/.test(node.component);
  if (isComposite) imports.add(node.component);

  const attrParts = formatAttrParts(node.props, node.expressions);
  const inlineAttrs = attrParts.length > 0 ? ' ' + attrParts.join(' ') : '';

  // "Compact" elements — a single short text child, or inline
  // expression content — get attrs inline regardless of length.
  // Readability gain (long classNames on a header don't outweigh
  // the ceremony of multi-line attrs for a one-word label).
  const hasInlineExpression = Boolean(
    node.expressions?.children && (!node.children || node.children.length === 0)
  );
  const hasSingleTextChild = Boolean(
    node.children?.length === 1 &&
    node.children[0].component === '__text__' &&
    !node.children[0].iterate
  );
  const isCompact = hasInlineExpression || hasSingleTextChild;

  const shouldBreakAttrs = !isCompact && inlineAttrs.length > ATTR_BREAK_WIDTH;
  const multiLineAttrs = attrParts.length > 0
    ? '\n' + attrParts.map(a => ' '.repeat(indent + 2) + a).join('\n') + '\n' + pad
    : '';
  const attrs = shouldBreakAttrs ? multiLineAttrs : inlineAttrs;

  // Inline-expression content: `<tag>{expr}</tag>` on one line.
  if (hasInlineExpression) {
    return `${pad}<${node.component}${attrs}>{${node.expressions!.children}}</${node.component}>`;
  }

  // Single-text-child shortcut: `<tag>text</tag>` on one line.
  if (hasSingleTextChild) {
    const text = (node.children![0].props?.text as string) ?? '';
    return `${pad}<${node.component}${attrs}>${text}</${node.component}>`;
  }

  const bodyLines = renderChildren(node, indent + 2, imports);

  // Void elements — no children allowed, self-closing.
  if (VOID_ELEMENTS.has(node.component) && bodyLines === null) {
    return `${pad}<${node.component}${attrs} />`;
  }

  // No children → empty element.
  if (bodyLines === null || bodyLines === '') {
    return `${pad}<${node.component}${attrs} />`;
  }

  // Multi-line element with nested children.
  return [
    `${pad}<${node.component}${attrs}>`,
    bodyLines,
    `${pad}</${node.component}>`,
  ].join('\n');
}

/**
 * Render children as a string. Respects `iterate` — wraps children
 * in a `.map(...)` when present. Returns `null` if the node has no
 * content at all (caller may decide to self-close).
 */
function renderChildren(node: RenderNode, indent: number, imports: Set<string>): string | null {
  const pad = ' '.repeat(indent);
  const children = node.children ?? [];

  if (node.iterate) {
    const { source, itemName, keyExpression } = node.iterate;
    // The iterate wraps the WHOLE element-with-its-children, not
    // just the children list. Caller (emitNode) uses the fact that
    // `iterate` is present on the node itself; this path is
    // unreachable in the current shape.
    // Placeholder — iterate is handled by the parent context below.
    return children.map(c => emitNode(c, indent, imports)).join('\n');
  }

  if (children.length === 0 && !node.expressions?.children) return null;
  if (children.length === 0 && node.expressions?.children) return '';

  return children
    .map(c => {
      if (c.iterate) return emitIterated(c, indent, imports);
      return emitNode(c, indent, imports);
    })
    .join('\n');
}

/**
 * Emit a node whose `iterate` produces N instances. The form is
 * `{source.map((item, idx) => ( <element>...</element> ))}`.
 */
function emitIterated(node: RenderNode, indent: number, imports: Set<string>): string {
  const pad = ' '.repeat(indent);
  const { source, itemName, keyExpression } = node.iterate!;
  // Strip iterate so emitNode doesn't see it and re-wrap.
  const inner = emitNode({ ...node, iterate: undefined }, indent + 2, imports);
  const args = keyExpression ? `(${itemName}, ${keyExpression})` : `(${itemName})`;
  return [
    `${pad}{${source}.map(${args} => (`,
    inner,
    `${pad}))}`,
  ].join('\n');
}

/**
 * Return each attribute as its own formatted string — caller
 * decides whether to join with spaces (inline) or newlines
 * (multi-line readability).
 */
function formatAttrParts(
  props: Record<string, unknown> | undefined,
  expressions: Record<string, string> | undefined,
): string[] {
  const parts: string[] = [];

  for (const [k, v] of Object.entries(props ?? {})) {
    if (v === undefined || v === null) continue;
    parts.push(formatStaticProp(k, v));
  }

  for (const [k, expr] of Object.entries(expressions ?? {})) {
    if (k === 'children') continue;   // element content, not attr
    parts.push(`${k}={${expr}}`);
  }

  return parts;
}

function formatStaticProp(name: string, value: unknown): string {
  if (value === true) return name;
  if (typeof value === 'string') return `${name}=${JSON.stringify(value)}`;
  if (typeof value === 'number' || typeof value === 'boolean') {
    return `${name}={${value}}`;
  }
  return `${name}={${JSON.stringify(value)}}`;
}

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