const fs = require("node:fs");
const path = require("node:path");
const readline = require("node:readline");
const os = require("node:os");

const DEFAULT_LIMIT = 80;

function argValue(name, fallback = "") {
  const prefix = `--${name}=`;
  const match = process.argv.find((entry) => entry.startsWith(prefix));
  return match ? match.slice(prefix.length) : fallback;
}

function defaultReviewSamplesPath() {
  const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
  return path.join(appData, "genshin-artifact-assistant", "review-samples.jsonl");
}

function simplifyId(value) {
  return String(value || "")
    .replace(/[^0-9A-Za-z]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80) || "unknown";
}

function parsedSummary(parsed) {
  if (!parsed || typeof parsed !== "object") return {};
  return {
    name: parsed.name,
    slot: parsed.slot,
    level: parsed.level,
    mainStat: parsed.mainStat,
    mainValue: parsed.mainValue,
    setName: parsed.setName,
    equipped: parsed.equipped,
    substats: Array.isArray(parsed.substats) ? parsed.substats : [],
    confidence: parsed.confidence,
    notes: Array.isArray(parsed.notes) ? parsed.notes : [],
  };
}

const REQUIRED_FAST_OCR_FIELDS = [
  "artifact-name",
  "artifact-slot",
  "artifact-main-stat-label",
  "artifact-level",
  "artifact-substats",
];

function ocrMap(record) {
  const entries = record?.sample?.capture?.ocr;
  if (!Array.isArray(entries) || entries.length === 0) return null;
  const result = {};
  for (const entry of entries) {
    if (typeof entry?.id === "string" && typeof entry?.text === "string") {
      result[entry.id] = entry.text;
    }
  }
  return Object.keys(result).length ? result : null;
}

function candidateFromRecord(record, index) {
  const ocr = ocrMap(record);
  if (!ocr) return null;
  const parsed = parsedSummary(record?.sample?.parsed);
  const reason = record?.sample?.reason || "missing-reason";
  const savedAt = record?.savedAt || "unknown";
  const capture = record?.sample?.capture || {};
  const ocrFieldIds = Object.keys(ocr).sort();
  const missingFastFields = REQUIRED_FAST_OCR_FIELDS.filter((field) => !ocr[field]);
  const hasEquippedFooterOcr = Boolean(ocr["artifact-footer"]);
  const locked = typeof capture.locked === "boolean" ? capture.locked : null;
  return {
    id: `review-${simplifyId(savedAt)}-${index}`,
    savedAt,
    reason,
    confirmed: false,
    resolution: capture.width && capture.height ? `${capture.width}x${capture.height}` : "",
    ocrFieldIds,
    missingFastFields,
    hasEquippedFooterOcr,
    locked,
    likelyStaleCapture: missingFastFields.includes("artifact-slot"),
    parsed,
    ocr,
    reviewPrompt: {
      action: "Confirm or correct parsed fields before moving this case into src/eval/corpus/confirmedReviewCorpus.ts.",
      expectedFields: {
        name: parsed.name || "",
        slot: parsed.slot || "",
        level: parsed.level ?? "",
        mainStat: parsed.mainStat || "",
        mainValue: parsed.mainValue || "",
        setName: parsed.setName || "",
        equipped: parsed.equipped || "",
        substats: parsed.substats || [],
      },
      validationChecks: {
        equipped: hasEquippedFooterOcr ? "Confirm character name or mark Not detected." : "No footer OCR in this sample.",
        locked: locked === null ? "No lock-state payload in this sample." : `Confirm locked=${locked}.`,
      },
    },
  };
}

function candidateKey(candidate) {
  return [
    candidate.reason,
    candidate.parsed.name,
    candidate.parsed.slot,
    candidate.parsed.mainStat,
    candidate.parsed.setName,
    JSON.stringify(candidate.parsed.substats || []),
    JSON.stringify(candidate.ocr),
  ].join("\u001f");
}

function candidatePriority(candidate) {
  const reason = candidate.reason || "";
  const parsed = candidate.parsed || {};
  const missingCount = candidate.missingFastFields?.length || 0;
  const hasUnknownCritical = [parsed.name, parsed.slot, parsed.mainStat, parsed.setName].some((value) => String(value || "").startsWith("Unknown"));
  if (candidate.likelyStaleCapture) return 4;
  if (missingCount === 0 && /low-field|low-total|capture-rejected|initial-selection/i.test(reason)) return 0;
  if (missingCount === 0 && (parsed.notes || []).some((note) => /fuzzy|incomplete|not confidently|low/i.test(note))) return 1;
  if (missingCount === 0) return 2;
  if (missingCount <= 1 && !hasUnknownCritical) return 3;
  return 5;
}

async function readCandidates(inputPath, limit) {
  const candidates = [];
  const seen = new Set();
  let total = 0;
  let invalid = 0;
  const rl = readline.createInterface({ input: fs.createReadStream(inputPath, { encoding: "utf8" }) });
  for await (const line of rl) {
    if (!line.trim()) continue;
    total++;
    let record;
    try {
      record = JSON.parse(line);
    } catch {
      invalid++;
      continue;
    }
    const candidate = candidateFromRecord(record, total);
    if (!candidate) continue;
    const key = candidateKey(candidate);
    if (seen.has(key)) continue;
    seen.add(key);
    candidates.push(candidate);
  }
  candidates.sort((left, right) => {
    const priority = candidatePriority(left) - candidatePriority(right);
    if (priority) return priority;
    const missingDiff = (left.missingFastFields?.length || 0) - (right.missingFastFields?.length || 0);
    if (missingDiff) return missingDiff;
    return String(right.savedAt).localeCompare(String(left.savedAt));
  });
  return { total, invalid, candidates: candidates.slice(0, limit), uniqueCandidates: candidates.length };
}

function increment(map, key) {
  const normalizedKey = key || "unknown";
  map[normalizedKey] = (map[normalizedKey] || 0) + 1;
}

function buildExportStats(candidates) {
  const reasonCounts = {};
  const missingFastFieldCounts = {};
  let completeFastFields = 0;
  let likelyStaleCaptures = 0;
  let equippedFooterCandidates = 0;
  let lockedTrueCandidates = 0;
  let lockedFalseCandidates = 0;
  for (const candidate of candidates) {
    increment(reasonCounts, candidate.reason);
    if (candidate.likelyStaleCapture) likelyStaleCaptures++;
    if (candidate.hasEquippedFooterOcr) equippedFooterCandidates++;
    if (candidate.locked === true) lockedTrueCandidates++;
    if (candidate.locked === false) lockedFalseCandidates++;
    const missingFields = candidate.missingFastFields || [];
    if (missingFields.length === 0) completeFastFields++;
    for (const field of missingFields) increment(missingFastFieldCounts, field);
  }
  return {
    completeFastFields,
    likelyStaleCaptures,
    equippedFooterCandidates,
    lockedTrueCandidates,
    lockedFalseCandidates,
    reasonCounts,
    missingFastFieldCounts,
  };
}

function markdownFor(summary, candidates) {
  const lines = [
    "# Review Eval Candidates",
    "",
    `Source: ${summary.inputPath}`,
    `Generated: ${summary.generatedAt}`,
    `Records read: ${summary.recordsRead}`,
    `Unique candidates: ${summary.uniqueCandidates}`,
    `Exported candidates: ${summary.exportedCandidates}`,
    `Complete fast-field candidates: ${summary.exportStats.completeFastFields}`,
    `Likely stale captures: ${summary.exportStats.likelyStaleCaptures}`,
    `Equipped footer candidates: ${summary.exportStats.equippedFooterCandidates}`,
    `Locked=true candidates: ${summary.exportStats.lockedTrueCandidates}`,
    `Locked=false candidates: ${summary.exportStats.lockedFalseCandidates}`,
    "",
    "These cases are not ground truth yet. Confirm or correct the expected fields before committing any case into `src/eval/corpus/`.",
    "",
    "## Export stats",
    "",
    "Reason counts:",
    "",
    ...Object.entries(summary.exportStats.reasonCounts).map(([reason, count]) => `- ${reason}: ${count}`),
    "",
    "Missing fast-field counts:",
    "",
    ...Object.entries(summary.exportStats.missingFastFieldCounts).map(([field, count]) => `- ${field}: ${count}`),
    "",
  ];
  for (const candidate of candidates) {
    lines.push(`## ${candidate.id}`);
    lines.push("");
    lines.push(`- savedAt: ${candidate.savedAt}`);
    lines.push(`- reason: ${candidate.reason}`);
    lines.push(`- missingFastFields: ${candidate.missingFastFields.join(", ") || "none"}`);
    lines.push(`- likelyStaleCapture: ${candidate.likelyStaleCapture ? "yes" : "no"}`);
    lines.push(`- hasEquippedFooterOcr: ${candidate.hasEquippedFooterOcr ? "yes" : "no"}`);
    lines.push(`- locked: ${candidate.locked === null ? "unknown" : candidate.locked}`);
    lines.push(`- parsed: ${candidate.parsed.name || "?"} | ${candidate.parsed.slot || "?"} | ${candidate.parsed.mainStat || "?"} | ${candidate.parsed.setName || "?"} | equipped=${candidate.parsed.equipped || "?"}`);
    lines.push(`- substats: ${(candidate.parsed.substats || []).join(", ") || "?"}`);
    lines.push("");
    lines.push("OCR:");
    for (const [id, text] of Object.entries(candidate.ocr)) {
      lines.push(`- ${id}: ${JSON.stringify(text)}`);
    }
    lines.push("");
  }
  return `${lines.join("\n")}\n`;
}

async function main() {
  const inputPath = path.resolve(argValue("input", defaultReviewSamplesPath()));
  const outputDir = path.resolve(argValue("out", path.join(process.cwd(), "outputs", "review-eval-candidates")));
  const limit = Math.max(1, Math.min(500, Number(argValue("limit", String(DEFAULT_LIMIT))) || DEFAULT_LIMIT));
  if (!fs.existsSync(inputPath)) {
    throw new Error(`Review sample file not found: ${inputPath}`);
  }
  fs.mkdirSync(outputDir, { recursive: true });
  const result = await readCandidates(inputPath, limit);
  const summary = {
    inputPath,
    outputDir,
    generatedAt: new Date().toISOString(),
    recordsRead: result.total,
    invalidRecords: result.invalid,
    uniqueCandidates: result.uniqueCandidates,
    exportedCandidates: result.candidates.length,
    exportStats: buildExportStats(result.candidates),
  };
  const payload = { summary, candidates: result.candidates };
  const jsonPath = path.join(outputDir, "review-eval-candidates.json");
  const mdPath = path.join(outputDir, "review-eval-candidates.md");
  fs.writeFileSync(jsonPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
  fs.writeFileSync(mdPath, markdownFor(summary, result.candidates), "utf8");
  console.log(JSON.stringify({ ok: true, ...summary, jsonPath, mdPath }, null, 2));
}

if (require.main === module) {
  main().catch((error) => {
    console.error(error instanceof Error ? error.message : String(error));
    process.exit(1);
  });
}

module.exports = {
  buildExportStats,
  candidateFromRecord,
  candidatePriority,
  defaultReviewSamplesPath,
  markdownFor,
  readCandidates,
};
