/**
 * SET 47b — Freeze-time suite ↔ stub-scaffold import wiring (0 tok).
 *
 * Cross-feature scaffold-owned symbols referenced as free identifiers in a
 * suite must receive imports at freeze (and re-freeze). Ownership makes the
 * import site unique; repairs must never chase this with impl redraws.
 */
import type { FeatureSpec, GeneratedFile } from '../core/types';

const KEYWORDS = new Set([
  'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
  'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for',
  'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'null', 'return',
  'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'undefined', 'var',
  'void', 'while', 'with', 'yield', 'await', 'async', 'of', 'as', 'from', 'type',
  'interface', 'implements', 'private', 'public', 'protected', 'readonly', 'static',
  'get', 'set', 'describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach',
  'vi', 'jest', 'spyOn', 'fn',
]);

function keyOf(p: string): string {
  return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\.(ts|tsx|js|jsx)$/, '');
}

function relativeSpec(importerPath: string, targetPath: string): string {
  const importerKey = keyOf(importerPath);
  const targetKey = keyOf(targetPath);
  const importerDir = importerKey.split('/').slice(0, -1);
  const targetDir = targetKey.split('/').slice(0, -1);
  if (importerDir.join('/') === targetDir.join('/')) {
    return `./${targetKey.split('/').pop()}`;
  }
  const ups = importerDir.length;
  return `${'../'.repeat(ups)}${targetKey}`;
}

/** Symbol → owning stub/export file (unique by construction from the spec). */
export function scaffoldOwnershipMap(spec: FeatureSpec): Map<string, string> {
  const map = new Map<string, string>();
  for (const feature of spec.features) {
    for (const ex of feature.exports ?? []) {
      if (ex.symbol && ex.file && !map.has(ex.symbol)) map.set(ex.symbol, ex.file);
    }
  }
  const declRe = (sym: string) => new RegExp(
    `export\\s+(?:default\\s+)?(?:async\\s+)?(?:function\\*?|class|const|let|var|type|interface|enum)\\s+${sym}\\b`,
  );
  for (const feature of spec.features) {
    for (const stub of feature.stubs ?? []) {
      for (const m of stub.content.matchAll(
        /export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)\b/g,
      )) {
        const sym = m[1]!;
        if (!map.has(sym) && declRe(sym).test(stub.content)) map.set(sym, stub.path);
      }
    }
  }
  return map;
}

export function isScaffoldOwnedSymbol(spec: FeatureSpec, symbol: string): boolean {
  return scaffoldOwnershipMap(spec).has(symbol);
}

function stripStringsAndComments(src: string): string {
  let out = '';
  let i = 0;
  while (i < src.length) {
    const c = src[i]!;
    if (c === '/' && src[i + 1] === '/') {
      out += ' ';
      i += 2;
      while (i < src.length && src[i] !== '\n') { out += ' '; i++; }
      continue;
    }
    if (c === '/' && src[i + 1] === '*') {
      out += '  ';
      i += 2;
      while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
        out += src[i] === '\n' ? '\n' : ' ';
        i++;
      }
      if (i < src.length) { out += '  '; i += 2; }
      continue;
    }
    if (c === '"' || c === "'" || c === '`') {
      const q = c;
      out += ' ';
      i++;
      while (i < src.length && src[i] !== q) {
        if (src[i] === '\\') { out += '  '; i += 2; continue; }
        out += src[i] === '\n' ? '\n' : ' ';
        i++;
      }
      if (i < src.length) { out += ' '; i++; }
      continue;
    }
    out += c;
    i++;
  }
  return out;
}

function alreadyImported(content: string, sym: string): boolean {
  const reNamed = new RegExp(
    `import\\s*(?:type\\s*)?\\{[^}]*\\b${sym}\\b[^}]*\\}\\s*from\\s*['"][^'"]+['"]`,
  );
  const reDefault = new RegExp(`import\\s+${sym}\\b\\s*from\\s*['"][^'"]+['"]`);
  return reNamed.test(content) || reDefault.test(content);
}

function freeIdentifiers(content: string): Set<string> {
  const stripped = stripStringsAndComments(content);
  const ids = new Set<string>();
  for (const m of stripped.matchAll(/\b([A-Za-z_$][\w$]*)\b/g)) {
    const id = m[1]!;
    if (KEYWORDS.has(id)) continue;
    ids.add(id);
  }
  return ids;
}

export type SuiteWireResult = {
  content: string;
  injected: { symbol: string; from: string }[];
};

/**
 * Inject missing imports for free identifiers that match stub-scaffold ownership.
 * Deterministic: symbols sorted, one import line per owner file (merged).
 */
export function wireSuiteScaffoldImports(
  suiteContent: string,
  suitePath: string,
  spec: FeatureSpec,
): SuiteWireResult {
  const ownership = scaffoldOwnershipMap(spec);
  if (!ownership.size) return { content: suiteContent, injected: [] };

  const free = freeIdentifiers(suiteContent);
  const needed: { symbol: string; file: string }[] = [];
  for (const [sym, file] of [...ownership.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
    if (!free.has(sym)) continue;
    if (alreadyImported(suiteContent, sym)) continue;
    needed.push({ symbol: sym, file });
  }
  if (!needed.length) return { content: suiteContent, injected: [] };

  // Merge by owner file.
  const byFile = new Map<string, string[]>();
  for (const n of needed) {
    const list = byFile.get(n.file) ?? [];
    list.push(n.symbol);
    byFile.set(n.file, list);
  }

  const lines: string[] = [];
  const injected: { symbol: string; from: string }[] = [];
  for (const file of [...byFile.keys()].sort((a, b) => a.localeCompare(b))) {
    const syms = byFile.get(file)!.slice().sort((a, b) => a.localeCompare(b));
    const from = relativeSpec(suitePath, file);
    lines.push(`import { ${syms.join(', ')} } from '${from}';`);
    for (const symbol of syms) injected.push({ symbol, from });
  }

  return {
    content: `${lines.join('\n')}\n${suiteContent}`,
    injected,
  };
}

/** Apply wireSuiteScaffoldImports to a GeneratedFile suite. */
export function wireSuiteFile(suite: GeneratedFile, spec: FeatureSpec): SuiteWireResult & { file: GeneratedFile } {
  const r = wireSuiteScaffoldImports(suite.content, suite.path, spec);
  return { ...r, file: { path: suite.path, content: r.content } };
}
