import { parseArtifactCandidate } from "../lib/artifactOcrParser";
import type { CaptureResult, OcrResult } from "../types/global";

// Field-level OCR accuracy harness. It runs the real parseArtifactCandidate
// over a labeled corpus and reports per-field accuracy, so OCR/layout/parser
// changes can be measured instead of guessed at. The corpus is seeded from the
// parser test cases and grows from human-confirmed review samples
// (see reviewSampleCorpus.ts). This is the gate every OCR change runs against.

export type EvalField =
  | "name"
  | "slot"
  | "level"
  | "mainStat"
  | "mainValue"
  | "setName"
  | "equipped"
  | "substats";

export const EVAL_FIELDS: EvalField[] = [
  "name",
  "slot",
  "level",
  "mainStat",
  "mainValue",
  "setName",
  "equipped",
  "substats",
];

// Fields that, if wrong, make an artifact record unusable. Reported separately
// so a regression in "equipped" (nice-to-have) does not read the same as a
// regression in "mainStat" (critical).
export const CRITICAL_FIELDS: EvalField[] = ["name", "slot", "mainStat", "mainValue", "setName"];

export interface OcrEvalCase {
  /** Stable identifier, unique within a corpus. */
  id: string;
  /** Crop id -> raw OCR text, exactly as the capture pipeline would produce it. */
  ocr: Record<string, string>;
  /** Ground-truth values. Only the provided fields are scored. */
  expect: Partial<{
    name: string;
    slot: string;
    level: number;
    mainStat: string;
    mainValue: string;
    setName: string;
    equipped: string;
    substats: string[];
  }>;
  meta?: {
    resolution?: string;
    source?: "test-seed" | "review-sample" | "manual";
    note?: string;
  };
}

export interface FieldResult {
  field: EvalField;
  expected: string;
  actual: string;
  correct: boolean;
}

export interface CaseResult {
  id: string;
  fields: FieldResult[];
  evaluatedFields: number;
  correctFields: number;
  /** True when every evaluated field on the case matched. */
  exact: boolean;
  meta?: OcrEvalCase["meta"];
}

export interface FieldAccuracy {
  field: EvalField;
  evaluated: number;
  correct: number;
  /** 0..1, or null when the corpus never labels this field. */
  accuracy: number | null;
}

export interface EvalReport {
  totalCases: number;
  exactCases: number;
  exactRate: number;
  evaluatedFields: number;
  correctFields: number;
  fieldAccuracy: number;
  /** Accuracy restricted to CRITICAL_FIELDS. */
  criticalFieldAccuracy: number;
  perField: FieldAccuracy[];
  cases: CaseResult[];
  failures: CaseResult[];
}

export function captureFromOcr(textById: Record<string, string>): CaptureResult {
  const ocr: OcrResult[] = Object.entries(textById).map(([id, text]) => ({
    id,
    label: id,
    text,
    confidence: 80,
  }));
  return {
    id: "eval",
    name: "eval capture",
    width: 1920,
    height: 1080,
    dataUrl: "",
    capturedAt: new Date(0).toISOString(),
    ocr,
  };
}

function normalizeSubstats(values: readonly string[]) {
  return [...values].map((value) => value.replace(/\s+/g, "").toLowerCase()).sort();
}

function substatsEqual(expected: readonly string[], actual: readonly string[]) {
  const left = normalizeSubstats(expected);
  const right = normalizeSubstats(actual);
  if (left.length !== right.length) return false;
  return left.every((value, index) => value === right[index]);
}

function evaluateCase(evalCase: OcrEvalCase): CaseResult {
  const parsed = parseArtifactCandidate(captureFromOcr(evalCase.ocr));
  const fields: FieldResult[] = [];
  const stringValues: Record<string, string> = parsed
    ? {
        name: parsed.name,
        slot: parsed.slot,
        mainStat: parsed.mainStat,
        mainValue: parsed.mainValue,
        setName: parsed.setName,
        equipped: parsed.equipped,
      }
    : {};

  for (const field of EVAL_FIELDS) {
    if (!(field in evalCase.expect)) continue;

    if (field === "substats") {
      const expected = evalCase.expect.substats ?? [];
      const actual = parsed?.substats ?? [];
      fields.push({
        field,
        expected: expected.join(", "),
        actual: actual.join(", "),
        correct: substatsEqual(expected, actual),
      });
      continue;
    }

    if (field === "level") {
      const expected = evalCase.expect.level;
      const actual = parsed?.level;
      fields.push({
        field,
        expected: String(expected),
        actual: parsed ? String(actual) : "<no-parse>",
        correct: parsed != null && actual === expected,
      });
      continue;
    }

    const expected = String(evalCase.expect[field] ?? "");
    const actual = parsed ? stringValues[field] ?? "" : "<no-parse>";
    fields.push({ field, expected, actual, correct: parsed != null && actual === expected });
  }

  const correctFields = fields.filter((entry) => entry.correct).length;
  return {
    id: evalCase.id,
    fields,
    evaluatedFields: fields.length,
    correctFields,
    exact: fields.length > 0 && correctFields === fields.length,
    meta: evalCase.meta,
  };
}

export function runOcrEval(cases: readonly OcrEvalCase[]): EvalReport {
  const caseResults = cases.map(evaluateCase);

  const perField: FieldAccuracy[] = EVAL_FIELDS.map((field) => {
    let evaluated = 0;
    let correct = 0;
    for (const caseResult of caseResults) {
      const entry = caseResult.fields.find((candidate) => candidate.field === field);
      if (!entry) continue;
      evaluated++;
      if (entry.correct) correct++;
    }
    return { field, evaluated, correct, accuracy: evaluated === 0 ? null : correct / evaluated };
  });

  const evaluatedFields = caseResults.reduce((sum, entry) => sum + entry.evaluatedFields, 0);
  const correctFields = caseResults.reduce((sum, entry) => sum + entry.correctFields, 0);
  const exactCases = caseResults.filter((entry) => entry.exact).length;

  const criticalEvaluated = perField
    .filter((entry) => CRITICAL_FIELDS.includes(entry.field))
    .reduce((sum, entry) => sum + entry.evaluated, 0);
  const criticalCorrect = perField
    .filter((entry) => CRITICAL_FIELDS.includes(entry.field))
    .reduce((sum, entry) => sum + entry.correct, 0);

  return {
    totalCases: caseResults.length,
    exactCases,
    exactRate: caseResults.length === 0 ? 1 : exactCases / caseResults.length,
    evaluatedFields,
    correctFields,
    fieldAccuracy: evaluatedFields === 0 ? 1 : correctFields / evaluatedFields,
    criticalFieldAccuracy: criticalEvaluated === 0 ? 1 : criticalCorrect / criticalEvaluated,
    perField,
    cases: caseResults,
    failures: caseResults.filter((entry) => !entry.exact),
  };
}

function pct(value: number | null) {
  if (value === null) return "  n/a";
  return `${(value * 100).toFixed(1)}%`.padStart(6, " ");
}

export function formatReport(report: EvalReport): string {
  const lines: string[] = [];
  lines.push("OCR eval report");
  lines.push("=".repeat(48));
  lines.push(`Cases:              ${report.totalCases}`);
  lines.push(`Exact-match cases:  ${report.exactCases}/${report.totalCases} (${pct(report.exactRate).trim()})`);
  lines.push(`Field accuracy:     ${pct(report.fieldAccuracy).trim()} (${report.correctFields}/${report.evaluatedFields})`);
  lines.push(`Critical fields:    ${pct(report.criticalFieldAccuracy).trim()}`);
  lines.push("");
  lines.push("Per field:");
  for (const entry of report.perField) {
    const critical = CRITICAL_FIELDS.includes(entry.field) ? "*" : " ";
    lines.push(`  ${critical} ${entry.field.padEnd(11)} ${pct(entry.accuracy)}  (${entry.correct}/${entry.evaluated})`);
  }

  if (report.failures.length > 0) {
    lines.push("");
    lines.push(`Failures (${report.failures.length}):`);
    for (const failure of report.failures) {
      const wrong = failure.fields.filter((entry) => !entry.correct);
      lines.push(`  - ${failure.id}`);
      for (const entry of wrong) {
        lines.push(`      ${entry.field}: expected "${entry.expected}" got "${entry.actual}"`);
      }
    }
  }

  return lines.join("\n");
}
