import { readFile } from "node:fs/promises";
import { join } from "node:path";

import { findingSchema, type Finding } from "../../../schema/src/records/finding.js";
import { reviewDecisionSchema, type ReviewDecision } from "../../../schema/src/decisions/decision.js";
import { toReportFindingRecord } from "../build/finding-presentation.js";
import * as coreDecisionStore from "../../../core/src/decisions/store.js";
import { atomicWriteUtf8, canonicalJson } from "./atomic.js";
import type {
  AcceptedFindingRecord,
  AcceptedFindingInput,
  PriorDecisionDiff,
  ReportFindingRecord,
} from "../build/types.js";

export class AcceptedFindingError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "AcceptedFindingError";
  }
}

type DecisionIndex = { schemaVersion: "decision-index/v1"; activeIds: string[] };
type DecisionStoreApi = {
  readDecisionIndex(root: string): Promise<DecisionIndex>;
  writeDecisionIndex(root: string, index: DecisionIndex): Promise<void>;
};

const decisionStore = coreDecisionStore as unknown as DecisionStoreApi;

function assertAcceptedPair(input: AcceptedFindingInput): void {
  findingSchema.parse(input.finding);
  reviewDecisionSchema.parse(input.decision);
  if (input.decision.verdict !== "not_defect") {
    throw new AcceptedFindingError("accepted finding requires a not_defect decision");
  }
  if (
    input.decision.findingId !== input.finding.id ||
    input.decision.evidenceFingerprint !== input.finding.evidenceFingerprint
  ) {
    throw new AcceptedFindingError("accepted decision must match finding identity and fingerprint");
  }
}

export function toAcceptedFindingRecord(input: AcceptedFindingInput): AcceptedFindingRecord {
  assertAcceptedPair(input);
  return {
    finding: toReportFindingRecord(input.finding),
    decision: input.decision,
    status: "accepted",
  };
}

export function priorDecisionDiff(
  finding: Finding,
  priorDecisions: ReviewDecision[],
): PriorDecisionDiff | undefined {
  const decision = priorDecisions
    .filter(
      (entry) =>
        entry.verdict === "not_defect" &&
        entry.findingId === finding.id &&
        entry.evidenceFingerprint !== finding.evidenceFingerprint,
    )
    .sort((left, right) => right.reviewedAt.localeCompare(left.reviewedAt))[0];
  if (decision === undefined) {
    return undefined;
  }
  return {
    priorDecisionId: decision.id,
    priorFingerprint: decision.evidenceFingerprint,
    currentFingerprint: finding.evidenceFingerprint,
    reviewer: decision.reviewer,
    reviewedAt: decision.reviewedAt,
    ...(decision.reason === undefined ? {} : { reason: decision.reason }),
    scope: decision.scope,
  };
}

export function toReopenedFindingRecord(
  finding: Finding,
  priorDecisions: ReviewDecision[],
): ReportFindingRecord {
  const reopenedFrom = priorDecisionDiff(finding, priorDecisions);
  return {
    ...toReportFindingRecord(finding),
    ...(reopenedFrom === undefined ? {} : { reopenedFrom }),
  };
}

function reportDataPath(reportDir: string): string {
  return join(reportDir, "data/report.json");
}

export async function revokeAcceptedDecision(
  reportDir: string,
  decisionStoreRoot: string | undefined,
  decisionId: string,
  revokedAt: string,
): Promise<AcceptedFindingRecord> {
  if (decisionStoreRoot === undefined || decisionStoreRoot.trim().length === 0) {
    throw new AcceptedFindingError("accepted decision revocation requires a decision store root");
  }
  const path = reportDataPath(reportDir);
  const parsed = JSON.parse(await readFile(path, "utf8")) as {
    acceptedFindings?: AcceptedFindingRecord[];
  };
  const acceptedFindings = parsed.acceptedFindings;
  if (!Array.isArray(acceptedFindings)) {
    throw new AcceptedFindingError("accepted finding audit data is unavailable");
  }
  const acceptedIndex = acceptedFindings.findIndex((entry) => entry.decision.id === decisionId);
  if (acceptedIndex < 0) {
    throw new AcceptedFindingError(`accepted decision not found: ${decisionId}`);
  }
  const current = acceptedFindings[acceptedIndex];
  if (current === undefined) {
    throw new AcceptedFindingError(`accepted decision not found: ${decisionId}`);
  }
  const next: AcceptedFindingRecord = {
    ...current,
    status: "revoked",
    revokedAt,
  };
  acceptedFindings[acceptedIndex] = next;
  parsed.acceptedFindings = acceptedFindings;
  await atomicWriteUtf8(path, `${canonicalJson(parsed)}\n`);
  const decisionIndex = await decisionStore.readDecisionIndex(decisionStoreRoot);
  if (!decisionIndex.activeIds.includes(decisionId)) {
    throw new AcceptedFindingError(`accepted decision is not active: ${decisionId}`);
  }
  await decisionStore.writeDecisionIndex(decisionStoreRoot, {
    ...decisionIndex,
    activeIds: decisionIndex.activeIds.filter((id) => id !== decisionId),
  });
  return next;
}
