/**
 * SET 65 — shell-locked codegen (flagged: gates.shellLock).
 *
 * The impl file FRAME — non-function stub lines plus every exported function
 * declaration head — is emitted deterministically from the stubs; the model's
 * draw contributes function BODIES only, extracted by brace matching and
 * re-injected into the frame. Duplicate declarations, stray top-level prose,
 * and parse poison outside function bodies are structurally impossible in a
 * locked file. A slot whose body cannot be extracted falls back to the stub
 * body (still-parseable, still-red) and is reported.
 */
import type { GeneratedFile } from '../core/types';

export interface FrameSlot {
  symbol: string;
  /** Full declaration head, verbatim from the stub (up to the opening brace). */
  head: string;
  /** Stub's own body — the parseable fallback when the draw lacks the slot. */
  stubBody: string;
}

export interface FileFrame {
  path: string;
  /** Stub lines outside function declarations (imports, consts, types). */
  header: string;
  slots: FrameSlot[];
}

/** Find `export [async] function NAME(...)... {` heads with brace-matched bodies. */
function scanFunctionDecls(source: string): { symbol: string; head: string; body: string; start: number; end: number }[] {
  const out: { symbol: string; head: string; body: string; start: number; end: number }[] = [];
  const re = /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g;
  for (const m of source.matchAll(re)) {
    const start = m.index!;
    const braceAt = source.indexOf('{', start);
    if (braceAt < 0) continue;
    let depth = 0;
    let end = -1;
    for (let i = braceAt; i < source.length; i++) {
      const ch = source[i]!;
      if (ch === '{') depth++;
      else if (ch === '}') {
        depth--;
        if (depth === 0) { end = i; break; }
      }
    }
    if (end < 0) continue;
    out.push({
      symbol: m[1]!,
      head: source.slice(start, braceAt).trimEnd(),
      body: source.slice(braceAt + 1, end),
      start,
      end: end + 1,
    });
  }
  return out;
}

/** Build the deterministic frame from a stub file; null when no functions. */
export function buildFileFrame(stub: GeneratedFile): FileFrame | null {
  const decls = scanFunctionDecls(stub.content);
  if (!decls.length) return null;
  let header = stub.content;
  // Remove declarations back-to-front so indices stay valid.
  for (const d of [...decls].sort((a, b) => b.start - a.start)) {
    header = header.slice(0, d.start) + header.slice(d.end);
  }
  return {
    path: stub.path,
    header: header.trim(),
    slots: decls.map(d => ({ symbol: d.symbol, head: d.head, stubBody: d.body.trim() })),
  };
}

/** Extract the drawn body for one slot symbol (first match wins). */
export function extractDrawnBody(draw: string, symbol: string): string | null {
  const decls = scanFunctionDecls(draw);
  const hit = decls.find(d => d.symbol === symbol);
  if (hit) return hit.body.trim();
  // Arrow-const form: `export const NAME = (...) => { ... }` or `... => expr;`
  const sym = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const arrow = new RegExp(`(?:export\\s+)?const\\s+${sym}\\s*=[^=>{;\\n]*=>\\s*`).exec(draw);
  if (arrow) {
    const at = arrow.index + arrow[0].length;
    if (draw[at] === '{') {
      let depth = 0;
      for (let i = at; i < draw.length; i++) {
        if (draw[i] === '{') depth++;
        else if (draw[i] === '}') { depth--; if (depth === 0) return draw.slice(at + 1, i).trim(); }
      }
      return null;
    }
    const end = draw.indexOf(';', at);
    const expr = draw.slice(at, end < 0 ? undefined : end).trim();
    return expr ? `return ${expr};` : null;
  }
  return null;
}

export interface LockedResult {
  content: string;
  /** Slots the draw did not provide — kept on stub bodies, reported. */
  missing: string[];
  /**
   * SET 73 — top-level declarations excluded because they do not structurally
   * balance: parse poison PREVENTED at the frame instead of healed later.
   * Never silent — the runner emits and counts these as substrate wins.
   */
  dropped: string[];
}

/**
 * SET 73 — import hoist. The frame header comes from the STUB, which carries
 * no dependency imports; a locked file that dropped the draw's imports would
 * strip every legitimate cross-module dependency (and silently erase
 * wrong-path import defects the substrate exists to heal). Import statements
 * are a closed grammar, so they are safe to carry over verbatim — anything
 * that is not a well-formed import line stays excluded.
 */
const IMPORT_LINE_RE =
  /^\s*import\s+(?:type\s+)?(?:[\w${},*\s]+\s+from\s+)?['"][^'"\n]+['"]\s*;?\s*$/;

export function extractDrawnImports(draw: string): string[] {
  const out: string[] = [];
  const seen = new Set<string>();
  for (const line of draw.split('\n')) {
    if (!IMPORT_LINE_RE.test(line)) continue;
    const norm = line.trim();
    if (seen.has(norm)) continue;
    seen.add(norm);
    out.push(norm);
  }
  return out;
}

/**
 * SET 73 — helper carry-over. A real implementation declares more than the
 * stub's exported functions: private helpers, classes, constants, types. A
 * lock that kept only the slots would silently delete them and manufacture
 * undefined-symbol crashes. Non-slot top-level declarations are carried over
 * verbatim, DEDUPED BY SYMBOL (first wins) — which is exactly what kills the
 * duplicate-declaration class without deleting legitimate code.
 */
/**
 * Cheap structural parse gate: delimiters balanced, never closing below zero
 * (`)(` fails), strings terminated. No evaluator needed, CSP-safe.
 */
export function isStructurallyBalanced(text: string): boolean {
  const stack: string[] = [];
  const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
  let quote: string | null = null;
  for (let i = 0; i < text.length; i++) {
    const c = text[i]!;
    if (quote) {
      if (c === '\\') { i++; continue; }
      if (c === quote) quote = null;
      continue;
    }
    if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
    if (c === '(' || c === '[' || c === '{') { stack.push(c); continue; }
    if (c === ')' || c === ']' || c === '}') {
      if (stack.pop() !== pairs[c]) return false; // unbalanced or wrong order
    }
  }
  return stack.length === 0 && quote == null;
}

export function extractExtraDeclarations(
  draw: string,
  slotSymbols: ReadonlySet<string>,
  dropped?: string[],
): { symbol: string; text: string }[] {
  const out: { symbol: string; text: string }[] = [];
  const seen = new Set<string>();
  const re = /^(?:export\s+)?(?:declare\s+)?(?:async\s+)?(function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm;
  for (const m of draw.matchAll(re)) {
    const kind = m[1]!;
    const symbol = m[2]!;
    if (slotSymbols.has(symbol) || seen.has(symbol)) continue;
    const start = m.index!;
    let end: number;
    const braceAt = draw.indexOf('{', start);
    const semiAt = draw.indexOf(';', start);
    const lineEnd = draw.indexOf('\n', start);
    if ((kind === 'function' || kind === 'class' || kind === 'interface' || kind === 'enum')
      && braceAt >= 0 && (semiAt < 0 || braceAt < semiAt)) {
      let depth = 0;
      end = draw.length;
      for (let i = braceAt; i < draw.length; i++) {
        if (draw[i] === '{') depth++;
        else if (draw[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
      }
    } else if (braceAt >= 0 && semiAt >= 0 && braceAt < semiAt && kind !== 'type') {
      // Arrow/object initializer: brace-match, then take the trailing `;`.
      let depth = 0;
      end = draw.length;
      for (let i = braceAt; i < draw.length; i++) {
        if (draw[i] === '{') depth++;
        else if (draw[i] === '}') { depth--; if (depth === 0) { end = Math.min(i + 2, draw.length); break; } }
      }
    } else {
      end = semiAt >= 0 ? semiAt + 1 : (lineEnd >= 0 ? lineEnd : draw.length);
    }
    const text = draw.slice(start, end).trim();
    // Carry-over is for legitimate helpers ONLY: a declaration that does not
    // structurally balance is exactly the parse poison the lock exists to
    // exclude, so it never rides along.
    if (!text) continue;
    if (!isStructurallyBalanced(text)) { dropped?.push(symbol); continue; }
    seen.add(symbol);
    out.push({ symbol, text });
  }
  return out;
}

/** Re-emit the file: frame + drawn bodies (stub fallback per missing slot). */
export function lockDrawToFrame(frame: FileFrame, draw: string): LockedResult {
  const missing: string[] = [];
  const parts: string[] = [];
  const imports = extractDrawnImports(draw)
    .filter(i => !frame.header.includes(i));
  if (imports.length) parts.push(imports.join('\n'));
  if (frame.header) parts.push(frame.header);
  // Helpers the draw declared that the stub frame has no slot for.
  const slotSymbols = new Set(frame.slots.map(s => s.symbol));
  const dropped: string[] = [];
  const extras = extractExtraDeclarations(draw, slotSymbols, dropped)
    .filter(d => !frame.header.includes(d.text));
  for (const d of extras) parts.push(d.text);
  for (const slot of frame.slots) {
    const body = extractDrawnBody(draw, slot.symbol);
    if (body == null) missing.push(slot.symbol);
    const indented = (body ?? slot.stubBody)
      .split('\n').map(l => (l.trim() ? `  ${l.replace(/^ {0,4}/, '')}` : l)).join('\n');
    parts.push(`${slot.head} {\n${indented}\n}`);
  }
  return { content: `${parts.join('\n\n')}\n`, missing, dropped };
}

export function formatShellLockPrevented(path: string, dropped: string[]): string {
  return `shell-lock: ${path} — parse poison PREVENTED at the frame: dropped unbalanced declaration(s) [${dropped.join(', ')}] (0 tok)`;
}

export function formatShellLock(path: string, slots: number): string {
  return `shell-lock: ${path} re-emitted through deterministic frame (${slots} slot${slots === 1 ? '' : 's'}, draw bodies only)`;
}

export function formatShellLockFallback(path: string, missing: string[]): string {
  return `shell-lock: ${path} — draw missing bodies [${missing.join(', ')}]; stub bodies kept for those slots`;
}
