import { createHash } from "node:crypto";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";

import { canonicalize } from "../../../schema/src/canonical.js";
import { REVIEW_DECISION_VERSION, type ReviewDecision } from "../../../schema/src/decisions/decision.js";
import { finalizeDecisions } from "@invariantum/core/decisions/finalize";
import type { ReportData } from "../build/types.js";
import { atomicWriteUtf8 } from "../review/atomic.js";
import { emptyReviewDraft, type ReviewDraft } from "../review/types.js";

export const FINALIZATION_VERIFICATION_COMMAND = "bash tools/gate.sh";

export class ReportFinalizationError extends Error {
  constructor(readonly code: string, message: string) {
    super(message);
    this.name = "ReportFinalizationError";
  }
}

export type FinalizationResult = {
  decisionCounts: { confirmedDefect: number; notDefect: number };
  packetPath?: string;
  unresolvedCount: number;
  verificationCommand: typeof FINALIZATION_VERIFICATION_COMMAND;
};

function hash(value: unknown): string {
  return createHash("sha256").update(canonicalize(value)).digest("hex");
}

function toDecisions(reportData: ReportData, draft: ReviewDraft, finalizedAt: string): ReviewDecision[] {
  if (draft.decisions.length === 0) {
    throw new ReportFinalizationError("no-staged-decisions", "finalization requires staged decisions");
  }
  const reportId = `report-${hash(reportData).slice(0, 16)}`;
  const findings = new Map(reportData.findings.map((finding) => [finding.id, finding]));
  const seen = new Set<string>();
  return draft.decisions.map((entry) => {
    if (seen.has(entry.findingId)) throw new ReportFinalizationError("duplicate-staged-decision", `duplicate staged decision for ${entry.findingId}`);
    seen.add(entry.findingId);
    const finding = findings.get(entry.findingId);
    if (finding === undefined) throw new ReportFinalizationError("unknown-finding", `staged decision references unknown finding ${entry.findingId}`);
    if (finding.evidenceFingerprint !== entry.evidenceFingerprint) throw new ReportFinalizationError("stale-evidence", `staged decision fingerprint is stale for ${entry.findingId}`);
    return {
      schemaVersion: REVIEW_DECISION_VERSION,
      id: `report-decision-${hash({ entry, reportId }).slice(0, 24)}`,
      findingId: entry.findingId,
      evidenceFingerprint: entry.evidenceFingerprint,
      verdict: entry.verdict,
      scope: entry.scope,
      reviewer: "report-reviewer",
      reviewedAt: finalizedAt,
      reportId,
      supersedes: [],
      ...(entry.reason === undefined ? {} : { reason: entry.reason }),
      ...(entry.promotion === undefined ? {} : { promotion: { contractId: entry.promotion.contractId, contractVersion: entry.promotion.contractVersion } }),
    };
  });
}

function reproduction(finding: ReportData["findings"][number]): string {
  const evidence = finding.evidence as Array<{ truthSource?: string; payload?: unknown }>;
  const details = evidence
    .map((entry) => entry.payload)
    .filter((payload): payload is Record<string, unknown> => payload !== null && typeof payload === "object");
  const route = details.map((payload) => payload.route).find((value): value is string => typeof value === "string") ?? "not recorded";
  const action = details.map((payload) => payload.action).find((value): value is string => typeof value === "string")
    ?? details.flatMap((payload): unknown[] => (Array.isArray(payload.actions) ? (payload.actions as unknown[]) : [])).find((value): value is string => typeof value === "string")
    ?? "reproduce recorded detector action";
  const cell = finding.context.kind === "browser" ? finding.context.cell.id : "not applicable";
  return `- Context: ${finding.context.kind}; route: ${route}; cell: ${cell}; action: ${action}`;
}

function section(finding: ReportData["findings"][number]): string {
  const contracts = finding.contractIds.length === 0 ? "None recorded" : finding.contractIds.join(", ");
  const artifacts = finding.artifacts.length === 0 ? ["  - None recorded"] : finding.artifacts.map((artifact) => `  - ${artifact.relativePath}`);
  return [
    `### Root-cause cluster: ${finding.id}`,
    "Keep this cluster separate: do not merge distinct evidence.",
    `- Finding ID: ${finding.id}`,
    "- Requirement IDs: REPORT-050, REPORT-051, REPORT-052, REPORT-053, REPORT-054",
    `- Contract IDs: ${contracts}`,
    `- Summary: ${finding.summary}`,
    "- Execution-context reproduction:",
    reproduction(finding),
    "- Screenshot and artifact paths:",
    ...artifacts,
    "- Measured evidence:",
    "```json",
    JSON.stringify(finding.evidence, null, 2),
    "```",
    `- Expected corrected invariant: ${finding.summary} no longer violates ${contracts}.`,
    "- Required regression tests: prove this finding identity, evidence fingerprint, and reproduction context after correction.",
  ].join("\n");
}

function packet(findings: ReportData["findings"], finalizedAt: string): string {
  return [
    "# Invariantum fix packet",
    "",
    "Audience: AI coding agent.",
    "",
    "## Scope and safety constraints",
    "- Work only on listed findings and preserve unrelated target source and evidence.",
    "- Fix every listed confirmed finding; do not stop after the first fix.",
    "- Do not suppress or weaken detectors, tests, contracts, or acceptance checks to obtain green.",
    "",
    `Finalized at: ${finalizedAt}`,
    "",
    "## Root-cause clusters",
    "",
    ...findings.map(section),
    "",
    "## Verification command",
    "",
    `\`${FINALIZATION_VERIFICATION_COMMAND}\``,
    "",
  ].join("\n");
}

async function writePacket(root: string, finalizedAt: string, contents: string): Promise<string> {
  const dir = join(root, ".invariantum", "fix-packets");
  const stamp = finalizedAt.replace(/[-:.]/g, "");
  await mkdir(dir, { recursive: true });
  for (let index = 0; ; index += 1) {
    const suffix = index === 0 ? "" : `-${String(index).padStart(3, "0")}`;
    const path = join(dir, `${stamp}-fix-packet${suffix}.md`);
    try {
      await writeFile(path, contents, { encoding: "utf8", flag: "wx" });
      return path;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
      throw error;
    }
  }
}

export async function finalizeReport(input: { reportDir: string; decisionStoreRoot: string; reportData: ReportData; draft: ReviewDraft; finalizedAt: string }): Promise<FinalizationResult> {
  if (input.decisionStoreRoot.trim().length === 0) throw new ReportFinalizationError("missing-decision-store", "finalization requires a decision store root");
  if (Number.isNaN(Date.parse(input.finalizedAt))) throw new ReportFinalizationError("invalid-finalized-at", "finalizedAt must be an ISO timestamp");
  const decisions = toDecisions(input.reportData, input.draft, input.finalizedAt);
  const confirmed = decisions.filter((decision) => decision.verdict === "confirmed_defect").map((decision) => input.reportData.findings.find((finding) => finding.id === decision.findingId)).filter((finding): finding is ReportData["findings"][number] => finding !== undefined);
  const packetPath = confirmed.length === 0 ? undefined : await writePacket(input.decisionStoreRoot, input.finalizedAt, packet(confirmed, input.finalizedAt));
  try {
    await finalizeDecisions(input.decisionStoreRoot, decisions);
  } catch (error) {
    if (packetPath !== undefined) await rm(packetPath, { force: true });
    throw error;
  }
  await atomicWriteUtf8(join(input.reportDir, ".invariantum", "review-draft.json"), `${canonicalize(emptyReviewDraft())}\n`);
  return {
    decisionCounts: { confirmedDefect: confirmed.length, notDefect: decisions.filter((decision) => decision.verdict === "not_defect").length },
    ...(packetPath === undefined ? {} : { packetPath }),
    unresolvedCount: input.reportData.findings.filter((finding) => !decisions.some((decision) => decision.findingId === finding.id)).length,
    verificationCommand: FINALIZATION_VERIFICATION_COMMAND,
  };
}
