import type { ReportFindingRecord, ReportData } from "./types.js";
import type { ReviewDraftView, ReviewVerdict, StagedDecisionView } from "./review-types.js";
import { parseContract, reviewDraftSchema } from "./report-schema.js";

const STATIC_DRAFT_KEY = "invariantum-static-review-draft/v1";

export function emptyStaticDraft(): ReviewDraftView {
  return { schemaVersion: "review-draft/v1", revision: 0, decisions: [], notes: {}, undoStacks: {} };
}

export function loadStaticDraft(): ReviewDraftView {
  try {
    const raw = window.localStorage.getItem(STATIC_DRAFT_KEY);
    if (raw === null) return emptyStaticDraft();
    return parseContract(reviewDraftSchema, JSON.parse(raw), "static review draft") as ReviewDraftView;
  } catch {
    return emptyStaticDraft();
  }
}

function saveStaticDraft(draft: ReviewDraftView): ReviewDraftView {
  window.localStorage.setItem(STATIC_DRAFT_KEY, JSON.stringify(draft));
  return draft;
}

function exactScope(finding: ReportFindingRecord): StagedDecisionView["scope"] {
  const dimensions: Array<{ key: string; value: string }> = [];
  if (finding.context.kind === "browser") {
    dimensions.push({ key: "cell", value: finding.context.cell.id });
    const evidence: unknown = finding.evidence;
    const payload: unknown = Array.isArray(evidence)
      ? (evidence as unknown[]).find((entry) => typeof entry === "object" && entry !== null && (entry as { truthSource?: unknown }).truthSource === "observed")
      : evidence;
    const candidate: unknown = typeof payload === "object" && payload !== null && "payload" in payload ? payload.payload : payload;
    if (typeof candidate === "object" && candidate !== null) {
      for (const key of ["route", "role", "locale"] as const) {
        const value = (candidate as Record<string, unknown>)[key];
        if (typeof value === "string" && value.length > 0) dimensions.push({ key, value });
      }
    }
  } else {
    dimensions.push({ key: "surface", value: finding.context.surfaceId }, { key: "adapter", value: finding.context.adapterId }, { key: "seed", value: finding.context.seed });
    if (finding.context.action !== undefined) dimensions.push({ key: "action", value: finding.context.action.id });
  }
  return {
    kind: "exact",
    detector: finding.detector,
    target: { kind: "exact", canonicalTarget: finding.target.canonical },
    context: { kind: finding.context.kind, dimensions: dimensions.sort((left, right) => left.key.localeCompare(right.key, "en", { sensitivity: "variant" })) },
    contractVersion: null,
  };
}

export function staticDecide(draft: ReviewDraftView, finding: ReportFindingRecord, verdict: ReviewVerdict, reason?: string): ReviewDraftView {
  const previous = draft.decisions.find((entry) => entry.findingId === finding.id);
  const entry: StagedDecisionView = { findingId: finding.id, evidenceFingerprint: finding.evidenceFingerprint, verdict, scope: previous?.scope ?? exactScope(finding), decidedAt: new Date().toISOString(), ...(reason === undefined ? {} : { reason }) };
  const undoEntry = previous === undefined ? {} : { verdict: previous.verdict, ...(previous.reason === undefined ? {} : { reason: previous.reason }) };
  return saveStaticDraft({ ...draft, revision: draft.revision + 1, decisions: [...draft.decisions.filter((item) => item.findingId !== finding.id), entry], undoStacks: { ...draft.undoStacks, [finding.id]: [...(draft.undoStacks[finding.id] ?? []), undoEntry] } });
}

export function staticClear(draft: ReviewDraftView, findingId: string): ReviewDraftView {
  return saveStaticDraft({ ...draft, revision: draft.revision + 1, decisions: draft.decisions.filter((entry) => entry.findingId !== findingId) });
}

export function staticUndo(draft: ReviewDraftView, findingId: string): ReviewDraftView {
  const stack = [...(draft.undoStacks[findingId] ?? [])];
  const prior = stack.pop();
  const decisions = draft.decisions.filter((entry) => entry.findingId !== findingId);
  const priorDecision = prior?.verdict === undefined ? undefined : draft.decisions.find((entry) => entry.findingId === findingId);
  if (priorDecision !== undefined && prior?.verdict !== undefined) decisions.push({ ...priorDecision, verdict: prior.verdict, ...(prior.reason === undefined ? {} : { reason: prior.reason }) });
  const undoStacks =
    stack.length === 0
      ? Object.fromEntries(Object.entries(draft.undoStacks).filter(([key]) => key !== findingId))
      : { ...draft.undoStacks, [findingId]: stack };
  return saveStaticDraft({ ...draft, revision: draft.revision + 1, decisions, undoStacks });
}

export async function exportStaticDecisionBundle(input: { draft: ReviewDraftView; metadata: NonNullable<ReportData["staticReview"]> }): Promise<string> {
  const body = { schemaVersion: "decision-bundle/v1", projectIdentity: input.metadata.projectIdentity, configHash: input.metadata.configHash, report: input.metadata.report, nonce: crypto.randomUUID(), expiresAt: new Date(Date.now() + 86_400_000).toISOString(), decisions: [...input.draft.decisions].sort((left, right) => left.findingId.localeCompare(right.findingId, "en", { sensitivity: "variant" })).map(({ findingId, evidenceFingerprint, verdict, scope, reason }) => ({ findingId, evidenceFingerprint, verdict, scope, ...(reason === undefined ? {} : { reason }) })) };
  return JSON.stringify({ ...body, bundleHash: await sha256(canonicalize(body)) }, null, 2);
}

async function sha256(value: string): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}

function canonicalize(value: unknown): string {
  return `${canonicalValue(value)}\n`;
}

function canonicalValue(value: unknown): string {
  if (value === null || typeof value === "boolean" || typeof value === "number") return JSON.stringify(value);
  if (typeof value === "string") return JSON.stringify(value.normalize("NFC"));
  if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
  if (typeof value === "object") return `{${Object.keys(value as Record<string, unknown>).sort(compareUnicodeScalars).map((key) => `${JSON.stringify(key.normalize("NFC"))}:${canonicalValue((value as Record<string, unknown>)[key])}`).join(",")}}`;
  throw new Error("unsupported decision bundle value");
}

function compareUnicodeScalars(left: string, right: string): number {
  const leftPoints = Array.from(left);
  const rightPoints = Array.from(right);
  for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index += 1) {
    const leftPoint = leftPoints[index];
    const rightPoint = rightPoints[index];
    if (leftPoint === undefined || rightPoint === undefined) break;
    const difference = (leftPoint.codePointAt(0) ?? 0) - (rightPoint.codePointAt(0) ?? 0);
    if (difference !== 0) return difference;
  }
  return leftPoints.length - rightPoints.length;
}
