/**
 * SET 43c — Export-conformance at the vet gate.
 *
 * Diff built impl exports vs stub contract (names + arity) and apply
 * mechanical fixes (alias / rename / missing stub-shaped export / sibling
 * re-export) before any measure. Deterministic; 0 tokens; no LLM.
 *
 * Integration: call `conformExportsToStubs(implFiles, stubs)` at the 36c
 * build-accept path, after a candidate write is owned/boundary-clean and
 * before transpile/probe/measure. Replace the candidate files with
 * `result.files` when `fixes.length > 0`.
 */
import type { GeneratedFile } from '../core/types';

export type ExportContract = { file: string; symbol: string; arity?: number | null };
export type ExportDiff =
  | { kind: 'missing'; symbol: string; stubFile: string; arity?: number | null }
  | { kind: 'renamed'; stubSymbol: string; implSymbol: string; stubFile: string; implFile: string }
  | { kind: 'arity-mismatch'; symbol: string; file: string; stubArity: number; implArity: number };

export type ParsedExport = { symbol: string; arity: number | null; kind: string };

const IDENT = /^[A-Za-z_$][\w$]*$/;

function normPath(p: string): string {
  return p.replace(/\\/g, '/').replace(/^\.\//, '');
}

function basename(p: string): string {
  const n = normPath(p);
  const i = n.lastIndexOf('/');
  return i >= 0 ? n.slice(i + 1) : n;
}

function dirname(p: string): string {
  const n = normPath(p);
  const i = n.lastIndexOf('/');
  return i >= 0 ? n.slice(0, i) : '';
}

/** Relative import spec from `fromFile` to sibling `toFile` (extensionless). */
function relativeSpec(fromFile: string, toFile: string): string {
  const fromDir = dirname(fromFile);
  const toNorm = normPath(toFile).replace(/\.(ts|tsx|js|jsx)$/, '');
  const toBase = basename(toNorm);
  if (dirname(toNorm) === fromDir || !fromDir) return `./${toBase}`;
  // Same-dir preference is the common stub/impl case; fall back to ./basename.
  return `./${toBase}`;
}

function editDistance(a: string, b: string): number {
  const m = a.length;
  const n = b.length;
  if (m === 0) return n;
  if (n === 0) return m;
  const prev = new Array<number>(n + 1);
  const cur = new Array<number>(n + 1);
  for (let j = 0; j <= n; j++) prev[j] = j;
  for (let i = 1; i <= m; i++) {
    cur[0] = i;
    for (let j = 1; j <= n; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      cur[j] = Math.min(prev[j]! + 1, cur[j - 1]! + 1, prev[j - 1]! + cost);
    }
    for (let j = 0; j <= n; j++) prev[j] = cur[j]!;
  }
  return prev[n]!;
}

/** True when names are case-variants, close edit distance, or common rename suffixes. */
function namesSimilar(stub: string, impl: string): boolean {
  if (stub === impl) return true;
  const a = stub.toLowerCase();
  const b = impl.toLowerCase();
  if (a === b) return true;
  const strip = (s: string) => s.replace(/(impl|fn|func|handler|export|_)+$/i, '');
  if (strip(a) === strip(b) && strip(a).length > 0) return true;
  if (b.startsWith(a) || a.startsWith(b)) {
    const longer = a.length >= b.length ? a : b;
    const shorter = a.length >= b.length ? b : a;
    if (shorter.length >= 2 && longer.length - shorter.length <= 4) return true;
  }
  const maxDist = Math.max(1, Math.min(3, Math.floor(Math.max(a.length, b.length) / 3)));
  return editDistance(a, b) <= maxDist;
}

/** Split a parameter list on top-level commas; returns null if unbalanced. */
function splitParams(paramsSrc: string): string[] | null {
  const out: string[] = [];
  let cur = '';
  let depth = 0;
  let inStr: string | null = null;
  for (let i = 0; i < paramsSrc.length; i++) {
    const c = paramsSrc[i]!;
    if (inStr) {
      cur += c;
      if (c === '\\') {
        i++;
        if (i < paramsSrc.length) cur += paramsSrc[i]!;
        continue;
      }
      if (c === inStr) inStr = null;
      continue;
    }
    if (c === '"' || c === "'" || c === '`') {
      inStr = c;
      cur += c;
      continue;
    }
    if (c === '(' || c === '[' || c === '{') {
      depth++;
      cur += c;
      continue;
    }
    if (c === ')' || c === ']' || c === '}') {
      depth--;
      if (depth < 0) return null;
      cur += c;
      continue;
    }
    if (c === ',' && depth === 0) {
      if (cur.trim()) out.push(cur.trim());
      cur = '';
      continue;
    }
    cur += c;
  }
  if (depth !== 0) return null;
  if (cur.trim()) out.push(cur.trim());
  return out;
}

/**
 * Declared arity of a param list. Rest-only / bare rest → null (flexible).
 * Optional/default params still count toward declared arity.
 */
function arityOfParams(paramsSrc: string): number | null {
  const trimmed = paramsSrc.trim();
  if (!trimmed) return 0;
  const parts = splitParams(trimmed);
  if (!parts) return null;
  if (parts.length === 1 && /^\.\.\./.test(parts[0]!)) return null;
  return parts.length;
}

/** Extract `(...)` param list starting at `(` index; returns inner + end index. */
function extractParenList(content: string, openIdx: number): { inner: string; end: number } | null {
  if (content[openIdx] !== '(') return null;
  let depth = 0;
  let inStr: string | null = null;
  for (let i = openIdx; i < content.length; i++) {
    const c = content[i]!;
    if (inStr) {
      if (c === '\\') { i++; continue; }
      if (c === inStr) inStr = null;
      continue;
    }
    if (c === '"' || c === "'" || c === '`') { inStr = c; continue; }
    if (c === '(') depth++;
    else if (c === ')') {
      depth--;
      if (depth === 0) return { inner: content.slice(openIdx + 1, i), end: i };
    }
  }
  return null;
}

function pushUnique(out: ParsedExport[], seen: Set<string>, entry: ParsedExport): void {
  if (!IDENT.test(entry.symbol) || seen.has(entry.symbol)) return;
  seen.add(entry.symbol);
  out.push(entry);
}

/**
 * Parse top-level exported function-like symbols with arity.
 * Recognizes: `export function`, `export async function`, `export const f = (…) =>`,
 * `export const f = function`, and `export { a, b as c }`.
 */
export function parseExportedFunctions(content: string): ParsedExport[] {
  const out: ParsedExport[] = [];
  const seen = new Set<string>();
  let depth = 0;
  let inStr: string | null = null;
  let i = 0;

  while (i < content.length) {
    const c = content[i]!;
    if (inStr) {
      if (c === '\\') { i += 2; continue; }
      if (c === inStr) inStr = null;
      i++;
      continue;
    }
    if (c === '"' || c === "'" || c === '`') { inStr = c; i++; continue; }
    if (c === '/' && content[i + 1] === '/') {
      while (i < content.length && content[i] !== '\n') i++;
      continue;
    }
    if (c === '/' && content[i + 1] === '*') {
      i += 2;
      while (i < content.length && !(content[i] === '*' && content[i + 1] === '/')) i++;
      i += 2;
      continue;
    }
    if (c === '{') { depth++; i++; continue; }
    if (c === '}') { depth = Math.max(0, depth - 1); i++; continue; }
    if (depth !== 0) { i++; continue; }

    const slice = content.slice(i);

    // export { a, b as c }  /  export { a } from '...'
    const braceExport = /^export\s*\{([^}]*)\}/.exec(slice);
    if (braceExport) {
      const body = braceExport[1]!;
      for (const part of body.split(',')) {
        const bit = part.trim();
        if (!bit || bit === 'type') continue;
        const asM = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(bit);
        if (asM) {
          pushUnique(out, seen, { symbol: asM[2]!, arity: null, kind: 'reexport' });
        } else {
          const idM = /^([A-Za-z_$][\w$]*)$/.exec(bit);
          if (idM) pushUnique(out, seen, { symbol: idM[1]!, arity: null, kind: 'reexport' });
        }
      }
      i += braceExport[0].length;
      continue;
    }

    // export async? function*? Name(
    const fnM = /^export\s+(?:default\s+)?(?:async\s+)?function\*?\s+([A-Za-z_$][\w$]*)\s*\(/.exec(slice);
    if (fnM) {
      const name = fnM[1]!;
      const openIdx = i + fnM[0].length - 1;
      const list = extractParenList(content, openIdx);
      const arity = list ? arityOfParams(list.inner) : null;
      pushUnique(out, seen, { symbol: name, arity, kind: 'function' });
      i = list ? list.end + 1 : i + fnM[0].length;
      continue;
    }

    // export const Name = async? (params) =>
    // export const Name = async? function*? (params)
    const constM = /^export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*/.exec(slice);
    if (constM) {
      const name = constM[1]!;
      let j = i + constM[0].length;
      const after = content.slice(j);
      const asyncArrow = /^async\s*\(/.exec(after);
      const arrow = /^\(/.exec(after);
      const asyncFn = /^async\s+function\*?\s*(?:[A-Za-z_$][\w$]*)?\s*\(/.exec(after);
      const fnExpr = /^function\*?\s*(?:[A-Za-z_$][\w$]*)?\s*\(/.exec(after);
      const bareIdent = /^([A-Za-z_$][\w$]*)\s*;?/.exec(after); // export const foo = bar
      let arity: number | null = null;
      let kind = 'const';
      if (asyncArrow || arrow || asyncFn || fnExpr) {
        const m = asyncArrow ?? arrow ?? asyncFn ?? fnExpr!;
        const openIdx = j + m[0].length - 1;
        const list = extractParenList(content, openIdx);
        arity = list ? arityOfParams(list.inner) : null;
        kind = 'const-function';
        j = list ? list.end + 1 : j + m[0].length;
      } else if (bareIdent) {
        arity = null;
        kind = 'alias';
        j += bareIdent[0].length;
      }
      pushUnique(out, seen, { symbol: name, arity, kind });
      i = j;
      continue;
    }

    i++;
  }
  return out;
}

function contractsFromStubs(stubs: GeneratedFile[]): ExportContract[] {
  const contracts: ExportContract[] = [];
  const seen = new Set<string>();
  const sorted = [...stubs].sort((a, b) => normPath(a.path).localeCompare(normPath(b.path)));
  for (const stub of sorted) {
    const file = normPath(stub.path);
    for (const ex of parseExportedFunctions(stub.content)) {
      const key = `${file}::${ex.symbol}`;
      if (seen.has(key)) continue;
      seen.add(key);
      contracts.push({ file, symbol: ex.symbol, arity: ex.arity });
    }
  }
  return contracts;
}

type ImplHit = { file: string; symbol: string; arity: number | null; kind: string };

function indexImplExports(implFiles: GeneratedFile[]): {
  bySymbol: Map<string, ImplHit[]>;
  byFile: Map<string, ParsedExport[]>;
} {
  const bySymbol = new Map<string, ImplHit[]>();
  const byFile = new Map<string, ParsedExport[]>();
  const sorted = [...implFiles].sort((a, b) => normPath(a.path).localeCompare(normPath(b.path)));
  for (const f of sorted) {
    const file = normPath(f.path);
    const parsed = parseExportedFunctions(f.content);
    byFile.set(file, parsed);
    for (const p of parsed) {
      const hit: ImplHit = { file, symbol: p.symbol, arity: p.arity, kind: p.kind };
      const list = bySymbol.get(p.symbol) ?? [];
      list.push(hit);
      bySymbol.set(p.symbol, list);
    }
  }
  return { bySymbol, byFile };
}

function stubSymbols(contracts: ExportContract[]): Set<string> {
  return new Set(contracts.map(c => c.symbol));
}

/** Best unused impl export that looks like a rename of `stubSymbol`. */
function findRenameCandidate(
  stubSymbol: string,
  stubFile: string,
  byFile: Map<string, ParsedExport[]>,
  claimed: Set<string>,
  stubSyms: Set<string>,
): ImplHit | null {
  const candidates: ImplHit[] = [];
  for (const [file, exports] of byFile) {
    for (const ex of exports) {
      if (stubSyms.has(ex.symbol) || claimed.has(ex.symbol)) continue;
      candidates.push({ file, symbol: ex.symbol, arity: ex.arity, kind: ex.kind });
    }
  }
  if (!candidates.length) return null;

  // Preferred file: only-one-extra export → renamed.
  const sameFile = candidates.filter(c => c.file === stubFile);
  const pool = sameFile.length ? sameFile : candidates;
  if (pool.length === 1) return pool[0]!;

  const scored = pool
    .filter(c => namesSimilar(stubSymbol, c.symbol))
    .map(c => ({
      c,
      dist: editDistance(stubSymbol.toLowerCase(), c.symbol.toLowerCase()),
      same: c.file === stubFile ? 0 : 1,
    }))
    .sort((a, b) => a.dist - b.dist || a.same - b.same || a.c.symbol.localeCompare(b.c.symbol));

  if (!scored.length) return null;
  // Unique best distance (no tie at rank 0).
  if (scored.length > 1 && scored[0]!.dist === scored[1]!.dist && scored[0]!.same === scored[1]!.same) {
    return null;
  }
  return scored[0]!.c;
}

export function diffExportsVsStubs(implFiles: GeneratedFile[], stubs: GeneratedFile[]): ExportDiff[] {
  const contracts = contractsFromStubs(stubs);
  const { bySymbol, byFile } = indexImplExports(implFiles);
  const stubSyms = stubSymbols(contracts);
  const claimed = new Set<string>(); // impl symbols already paired as renames
  const diffs: ExportDiff[] = [];

  for (const c of contracts) {
    const hits = bySymbol.get(c.symbol) ?? [];
    const preferred = hits.find(h => h.file === c.file) ?? hits[0];
    if (preferred) {
      claimed.add(preferred.symbol);
      const stubArity = c.arity;
      const implArity = preferred.arity;
      if (
        stubArity != null
        && implArity != null
        && stubArity !== implArity
      ) {
        diffs.push({
          kind: 'arity-mismatch',
          symbol: c.symbol,
          file: preferred.file,
          stubArity,
          implArity,
        });
      }
      continue;
    }

    const rename = findRenameCandidate(c.symbol, c.file, byFile, claimed, stubSyms);
    if (rename) {
      claimed.add(rename.symbol);
      diffs.push({
        kind: 'renamed',
        stubSymbol: c.symbol,
        implSymbol: rename.symbol,
        stubFile: c.file,
        implFile: rename.file,
      });
      continue;
    }

    diffs.push({
      kind: 'missing',
      symbol: c.symbol,
      stubFile: c.file,
      arity: c.arity ?? null,
    });
  }

  // Deterministic order: kind rank, then paths/symbols.
  const kindRank = { missing: 0, renamed: 1, 'arity-mismatch': 2 } as const;
  diffs.sort((a, b) => {
    const kr = kindRank[a.kind] - kindRank[b.kind];
    if (kr !== 0) return kr;
    if (a.kind === 'missing' && b.kind === 'missing') {
      return a.stubFile.localeCompare(b.stubFile) || a.symbol.localeCompare(b.symbol);
    }
    if (a.kind === 'renamed' && b.kind === 'renamed') {
      return a.stubFile.localeCompare(b.stubFile) || a.stubSymbol.localeCompare(b.stubSymbol);
    }
    if (a.kind === 'arity-mismatch' && b.kind === 'arity-mismatch') {
      return a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol);
    }
    return 0;
  });
  return diffs;
}

function ensureTrailingNewline(s: string): string {
  return s.endsWith('\n') ? s : `${s}\n`;
}

function appendExport(content: string, line: string): string {
  const base = content.replace(/\s*$/, '');
  if (!base) return ensureTrailingNewline(line);
  return ensureTrailingNewline(`${base}\n${line}`);
}

function stubShapedExport(symbol: string): string {
  return `export function ${symbol}(...args: unknown[]): unknown { throw new Error('stub'); }`;
}

function aliasLine(implSymbol: string, stubSymbol: string): string {
  // Prefer named re-export alias when both are idents.
  return `export { ${implSymbol} as ${stubSymbol} };`;
}

function findFile(files: Map<string, string>, path: string): string | undefined {
  const want = normPath(path);
  if (files.has(want)) return want;
  for (const k of files.keys()) {
    if (normPath(k) === want) return k;
  }
  return undefined;
}

/**
 * Widen a simple exported function's params toward stubArity when safe.
 * Returns patched content or null when unsafe / not found.
 */
function tryWidenArity(
  content: string,
  symbol: string,
  stubArity: number,
  implArity: number,
): string | null {
  if (stubArity === implArity) return content;
  // Only handle `export function Sym(...)` / `export async function Sym(...)`.
  const re = new RegExp(
    `(export\\s+(?:async\\s+)?function\\*?\\s+${symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*)\\(([^)]*)\\)`,
  );
  const m = re.exec(content);
  if (!m) return null;
  const params = m[2] ?? '';
  const parts = splitParams(params);
  if (!parts) return null;
  // Refuse complex params (destructure / rest already present when shrinking).
  if (parts.some(p => /[{[]/.test(p))) return null;

  let nextParts: string[];
  if (implArity < stubArity) {
    // Widen: append optional unknowns (or convert to rest if gap is large).
    if (stubArity - implArity >= 3 && parts.every(p => !/^\.\.\./.test(p))) {
      nextParts = [...parts, '...args: unknown[]'];
    } else {
      nextParts = [...parts];
      for (let n = implArity; n < stubArity; n++) {
        nextParts.push(`_a${n}?: unknown`);
      }
    }
  } else {
    // Shrink declared required arity: mark trailing extras optional.
    if (parts.some(p => /^\.\.\./.test(p))) return null;
    nextParts = parts.map((p, idx) => {
      if (idx < stubArity) return p;
      if (/\?/.test(p.split(':')[0] ?? '') || /=/.test(p)) return p;
      // insert ? after the ident
      const id = /^(\s*)([A-Za-z_$][\w$]*)(\s*)(:.*)?$/.exec(p);
      if (!id) return p;
      return `${id[1]}${id[2]}?${id[3] ?? ''}${id[4] ?? ': unknown'}`;
    });
  }

  const replacement = `${m[1]}(${nextParts.join(', ')})`;
  return content.slice(0, m.index) + replacement + content.slice(m.index + m[0].length);
}

/**
 * Apply mechanical fixes: alias/rename/add missing export re-export or
 * stub-shaped export wrapper. Returns patched impl files (new array).
 */
export function conformExportsToStubs(
  implFiles: GeneratedFile[],
  stubs: GeneratedFile[],
): {
  files: GeneratedFile[];
  fixes: { path: string; note: string }[];
} {
  const files = new Map<string, string>();
  for (const f of [...implFiles].sort((a, b) => normPath(a.path).localeCompare(normPath(b.path)))) {
    files.set(normPath(f.path), f.content);
  }

  const fixes: { path: string; note: string }[] = [];
  // Iterate until stable (rename then missing can unblock); cap at contracts+1.
  const maxPasses = contractsFromStubs(stubs).length + 2;
  for (let pass = 0; pass < maxPasses; pass++) {
    const snapshot: GeneratedFile[] = [...files.entries()].map(([path, content]) => ({ path, content }));
    const diffs = diffExportsVsStubs(snapshot, stubs);
    if (!diffs.length) break;

    let changed = false;
    for (const d of diffs) {
      if (d.kind === 'renamed') {
        const targetKey = findFile(files, d.stubFile) ?? normPath(d.stubFile);
        const content = files.get(targetKey) ?? '';
        // If impl lives elsewhere, re-export; if same file, alias locally.
        if (normPath(d.implFile) === normPath(d.stubFile)) {
          const line = aliasLine(d.implSymbol, d.stubSymbol);
          if (content.includes(line) || new RegExp(`\\bexport\\s*\\{[^}]*\\bas\\s+${d.stubSymbol}\\b`).test(content)) {
            continue;
          }
          files.set(targetKey, appendExport(content, line));
          fixes.push({
            path: targetKey,
            note: `aliased ${d.implSymbol} as ${d.stubSymbol}`,
          });
        } else {
          const spec = relativeSpec(d.stubFile, d.implFile);
          const line = `export { ${d.implSymbol} as ${d.stubSymbol} } from '${spec}';`;
          if (content.includes(line)) continue;
          files.set(targetKey, appendExport(content, line));
          fixes.push({
            path: targetKey,
            note: `re-exported ${d.implSymbol} as ${d.stubSymbol} from ${spec}`,
          });
        }
        changed = true;
        continue;
      }

      if (d.kind === 'missing') {
        const targetKey = findFile(files, d.stubFile) ?? normPath(d.stubFile);
        let content = files.get(targetKey);
        if (content === undefined) {
          content = '';
          files.set(targetKey, content);
        }

        // Prefer thin alias to a similarly-named export in the same file.
        const local = parseExportedFunctions(content);
        const localSimilar = local
          .filter(e => e.symbol !== d.symbol && namesSimilar(d.symbol, e.symbol))
          .sort((a, b) =>
            editDistance(d.symbol.toLowerCase(), a.symbol.toLowerCase())
            - editDistance(d.symbol.toLowerCase(), b.symbol.toLowerCase())
            || a.symbol.localeCompare(b.symbol));
        if (localSimilar.length === 1
          || (localSimilar.length > 1
            && editDistance(d.symbol.toLowerCase(), localSimilar[0]!.symbol.toLowerCase())
              < editDistance(d.symbol.toLowerCase(), localSimilar[1]!.symbol.toLowerCase()))) {
          const implSym = localSimilar[0]!.symbol;
          const line = aliasLine(implSym, d.symbol);
          if (!content.includes(line)) {
            files.set(targetKey, appendExport(content, line));
            fixes.push({ path: targetKey, note: `aliased ${implSym} as ${d.symbol}` });
            changed = true;
            continue;
          }
        }

        // Prefer re-export from a sibling that already exports the symbol.
        const siblingHit = [...files.entries()]
          .filter(([p]) => normPath(p) !== normPath(d.stubFile))
          .map(([p, c]) => ({ path: p, hit: parseExportedFunctions(c).find(e => e.symbol === d.symbol) }))
          .filter((x): x is { path: string; hit: ParsedExport } => !!x.hit)
          .sort((a, b) => a.path.localeCompare(b.path));
        if (siblingHit.length === 1) {
          const spec = relativeSpec(d.stubFile, siblingHit[0]!.path);
          const line = `export { ${d.symbol} } from '${spec}';`;
          if (!content.includes(line)) {
            files.set(targetKey, appendExport(content, line));
            fixes.push({
              path: targetKey,
              note: `re-exported ${d.symbol} from ${spec}`,
            });
            changed = true;
            continue;
          }
        }

        // Last resort: stub-shaped export wrapper.
        const line = stubShapedExport(d.symbol);
        if (!parseExportedFunctions(content).some(e => e.symbol === d.symbol)) {
          files.set(targetKey, appendExport(content, line));
          fixes.push({
            path: targetKey,
            note: `added missing export ${d.symbol}`,
          });
          changed = true;
        }
        continue;
      }

      if (d.kind === 'arity-mismatch') {
        const key = findFile(files, d.file);
        if (!key) {
          fixes.push({ path: d.file, note: `skipped arity fix for ${d.symbol}: file missing` });
          continue;
        }
        const content = files.get(key)!;
        const next = tryWidenArity(content, d.symbol, d.stubArity, d.implArity);
        if (next == null || next === content) {
          fixes.push({
            path: key,
            note: `skipped arity fix for ${d.symbol}: unsafe (${d.implArity}→${d.stubArity})`,
          });
          continue;
        }
        files.set(key, ensureTrailingNewline(next));
        fixes.push({
          path: key,
          note: `widened arity of ${d.symbol} ${d.implArity}→${d.stubArity}`,
        });
        changed = true;
      }
    }
    if (!changed) break;
  }

  const outFiles: GeneratedFile[] = [...files.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([path, content]) => ({ path, content }));

  // SET 47e — one note per symbol×file (multi-pass must not spam the log).
  const seenNotes = new Set<string>();
  const dedupedFixes: { path: string; note: string }[] = [];
  for (const fx of fixes) {
    const k = `${normPath(fx.path)}\0${fx.note}`;
    if (seenNotes.has(k)) continue;
    seenNotes.add(k);
    dedupedFixes.push(fx);
  }

  return { files: outFiles, fixes: dedupedFixes };
}
