/**
 * The diagnostic oracle — a registry indexed by the compiler's own finite,
 * published set of diagnostic codes.
 *
 * PREMISE (RHA): a programming language's error space is a discrete alphabet,
 * not an open set. TypeScript's checker emits ~500 distinct 2xxx (type) codes;
 * empirically a handful dominate machine-written code — type errors are ~94%
 * of LLM compile failures, and a small number of codes cover most instances
 * (the same power law seen in rustc: E0277+E0308 alone are >18%). So repair is
 * not open-ended generation. It is a QUICKSORT into a bounded partition, where
 * each cell declares three things:
 *
 *   route  — WHERE the fix belongs. The reporting file lies for cross-module
 *            contract errors: "print_renderer imports Page" reports TS2459 in
 *            print_renderer, but the fix (add `export`) is in page_manager.
 *   fix    — a DETERMINISTIC transform when the compiler has ALREADY told us
 *            the answer (add `export`; apply a "Did you mean 'X'" rename). Zero
 *            tokens, zero new stochastic surface — text is free.
 *   hint   — a class-scoped instruction that bounds the LLM's entropy to this
 *            one cell when the fix is genuinely semantic (type mismatch, etc.).
 *
 * Every handler is a permanent subtraction from the stochastic surface: the
 * registry only grows, the model's job only shrinks. The SAME shape generalizes
 * to any compiler with a finite code set — rustc E-codes, pyright, eslint rule
 * ids — each language gets its own registry, same three columns.
 */
import path from 'node:path';
import type { TscDiagnostic } from './sandbox';
import { ORACLE_BY_CODE } from './oracle.generated';
import { findLessons, makeClassKey, type LessonRecord } from './lessons';

const norm = (p: string) => p.replace(/\\/g, '/').replace(/^\.\//, '');
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

/** Bug-class label for a tsc diagnostic (TS####). */
export function classifyTsc(d: TscDiagnostic): string {
  return `TS${d.code}`;
}

/** Member/symbol named in a diagnostic message, when present. */
export function namedMember(d: TscDiagnostic): string | null {
  const m =
    /has no exported member(?: named)? ['"]?([A-Za-z_$][\w$]*)['"]?/.exec(d.message)
    ?? /Cannot find name ['"]?([A-Za-z_$][\w$]*)['"]?/.exec(d.message)
    ?? /Property ['"]?([A-Za-z_$][\w$]*)['"]? does not exist/.exec(d.message)
    ?? /Module ['"][^'"]+['"] has no exported member ['"]?([A-Za-z_$][\w$]*)['"]?/.exec(d.message)
    ?? /declares '([^']+)' locally/.exec(d.message);
  return m?.[1] ?? null;
}

/** Class key for grouping: bugClass|routedTarget|member? */
export function classKeyOf(d: TscDiagnostic): string {
  const target = routeTarget(d).file;
  return makeClassKey(classifyTsc(d), target, namedMember(d));
}

/** Resolve the module a diagnostic names (e.g. `'"./page_manager"'`) to a file. */
function definerOf(d: TscDiagnostic): string | null {
  const m = /Module\s+'"([^"]+)"'/.exec(d.message)?.[1];
  if (!m || !m.startsWith('.')) return null;
  const dir = path.posix.dirname(norm(d.file));
  return norm(path.posix.join(dir, m)) + '.ts';
}

export interface RepairTarget {
  /** Workspace-relative posix path of the file to edit. */
  file: string;
  /** True when the fix site differs from the reporting file (cross-module). */
  rerouted: boolean;
}

/** Codes whose fix belongs in the module they name, not the file that reports them. */
const EXPORT_CONTRACT_CODES = new Set([2459, 2305, 2614, 2724]);

/** WHERE should this diagnostic be fixed? */
export function routeTarget(d: TscDiagnostic): RepairTarget {
  if (EXPORT_CONTRACT_CODES.has(d.code)) {
    const def = definerOf(d);
    if (def) return { file: def, rerouted: true };
  }
  return { file: norm(d.file), rerouted: false };
}

/**
 * Apply a stored lesson mechanically (zero tokens). Dominant case: missing
 * member (TS2305/2304) — additive widen using the lesson's inferred signature.
 */
export function applyLessonFix(
  d: TscDiagnostic,
  content: string,
  lesson?: LessonRecord,
): string | null {
  const target = routeTarget(d).file;
  const member = namedMember(d);
  const ck = classKeyOf(d);
  const hit = lesson
    ?? findLessons(ck).find(l =>
      (!l.target || l.target === target) && (!l.member || l.member === member))
    // Member+class match across targets — the idiom generalizes; the stub
    // under repair is `content`'s file, not necessarily the lesson's original target.
    ?? findLessons(classifyTsc(d)).find(l =>
      !!(l.member && member && l.member === member));
  if (!hit || !member) return null;
  if (d.code !== 2305 && d.code !== 2304 && d.code !== 2339) return null;
  // Already present?
  if (new RegExp(`\\b(?:export\\s+)?(?:function|const|class|type|interface|enum)\\s+${escapeRe(member)}\\b`).test(content)
    || new RegExp(`\\b${escapeRe(member)}\\s*\\(`).test(content)
    || new RegExp(`export\\s*\\{[^}]*\\b${escapeRe(member)}\\b`).test(content)) {
    return null;
  }
  const sig = hit.signature?.trim()
    || `export function ${member}(..._args: unknown[]): never { throw new Error('stub: not implemented'); }`;
  const addition = sig.includes(member) ? sig : `export function ${member}(..._args: unknown[]): never { throw new Error('stub: not implemented'); }`;
  const next = content.trimEnd() + '\n\n' + addition + '\n';
  return next === content ? null : next;
}

/**
 * A deterministic fix when the compiler has already told us the answer.
 * Returns new file content, or null if this diagnostic has no mechanical fix.
 * `content` must be the content of `routeTarget(d).file`.
 * Consults loaded lessons AFTER the built-in mechanical patterns.
 */
export function deterministicFix(d: TscDiagnostic, content: string): string | null {
  // (0) Standing taxonomy from prior runs — documented idioms resolve mechanically.
  const fromLesson = applyLessonFix(d, content);
  if (fromLesson != null) return fromLesson;

  // (a) "declares 'X' locally, but it is not exported" (TS2459): add `export`.
  const notExported = /declares '([^']+)' locally, but it is not exported/.exec(d.message);
  if (notExported) {
    const sym = notExported[1];
    const kws = '(?:abstract\\s+)?(?:interface|type|class|const|let|var|function|enum)';
    if (new RegExp(`\\bexport\\s+${kws}\\s+${escapeRe(sym)}\\b`).test(content)) return null;
    const decl = new RegExp(`(^|\\n)([ \\t]*)(${kws}\\s+${escapeRe(sym)}\\b)`);
    if (decl.test(content)) return content.replace(decl, (_m, pre, ind, rest) => `${pre}${ind}export ${rest}`);
    return null;
  }

  // (b) "Did you mean 'Y'?" — the compiler's own suggestion. Rename the wrong
  //     identifier to the suggested one on the reporting line. This single
  //     pattern covers many codes (TS2551/2552/2724/2820/...): the checker's
  //     suggestion engine is a free deterministic oracle.
  const suggestion = /Did you mean '([^']+)'\?/.exec(d.message);
  const wrong = /(?:Cannot find name|Property '[^']*' does not exist[^']*|has no exported member(?: named)?)\s*'?([A-Za-z_$][\w$]*)'?/.exec(d.message)
    ?? /'([A-Za-z_$][\w$]*)'/.exec(d.message);
  if (suggestion && wrong && d.line >= 1) {
    const right = suggestion[1].replace(/^["']|["']$/g, '');
    const bad = wrong[1];
    if (bad && right && bad !== right && /^[A-Za-z_$][\w$]*$/.test(right)) {
      const lines = content.split('\n');
      const i = d.line - 1;
      const re = new RegExp(`\\b${escapeRe(bad)}\\b`);
      if (i < lines.length && re.test(lines[i])) {
        lines[i] = lines[i].replace(new RegExp(`\\b${escapeRe(bad)}\\b`, 'g'), right);
        return lines.join('\n');
      }
    }
  }

  // (c) "'X' is declared but its value is never read." (TS6133) on an IMPORT
  //     line — remove the unused specifier. Deterministic and side-effect-free
  //     (imports of stubs/tests carry no load-time effects). Only the import
  //     case is mechanical; an unused local falls through to the LLM.
  const unused = /'([^']+)' is declared but (?:its value is never read|never used)\./.exec(d.message);
  if (unused && d.line >= 1) {
    const sym = unused[1];
    const lines = content.split('\n');
    const i = d.line - 1;
    if (i < lines.length && /^\s*import\b/.test(lines[i])) {
      const line = lines[i];
      const braces = /\{([^}]*)\}/.exec(line);
      if (braces) {
        const names = braces[1].split(',').map(s => s.trim()).filter(Boolean);
        const kept = names.filter(n => n.replace(/\s+as\s+.*/, '').trim() !== sym && n !== sym);
        if (kept.length === names.length) return null; // symbol not in this import
        if (kept.length === 0) { lines.splice(i, 1); return lines.join('\n'); }
        lines[i] = line.replace(/\{[^}]*\}/, `{ ${kept.join(', ')} }`);
        return lines.join('\n');
      }
      if (new RegExp(`import\\s+(?:\\*\\s+as\\s+)?${escapeRe(sym)}\\b`).test(line)) {
        lines.splice(i, 1);
        return lines.join('\n');
      }
    }
    return null; // unused local, not an import — semantic
  }
  return null;
}

/**
 * A class-scoped instruction that bounds the LLM when no deterministic fix
 * applies. Read straight from the FROZEN, pre-verified oracle table
 * (oracle.generated.ts) — no runtime classification, no LLM in this path.
 */
export function repairHint(d: TscDiagnostic): string {
  return ORACLE_BY_CODE[d.code]?.hint ?? '';
}

/** Format a diagnostic with its scoped hint, for an LLM repair prompt. */
export function formatForRepair(d: TscDiagnostic): string {
  const h = repairHint(d);
  return `${norm(d.file)}:${d.line} TS${d.code}: ${d.message}${h ? ` → ${h}` : ''}`;
}
