import {
  closeSync,
  fsyncSync,
  openSync,
  readFileSync,
  renameSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { dirname } from "node:path";
import { manifestFingerprint } from "./journey-manifest.js";

export type OwnerUiProofId = "owner-authoring-ac-04-09" | "owner-approval-ac-10-11";

export interface OwnerUiReceipt {
  readonly schemaVersion: 1;
  readonly proofId: OwnerUiProofId;
  readonly seedFingerprint: string;
  readonly manifestStage: "seed" | "owner-created";
  readonly manifestGeneration: number;
  readonly entityIds: Readonly<{
    projectId: string;
    planId: string;
    planRevisionId: string;
    taskIds: readonly string[];
    factoryRunId?: string;
  }>;
  readonly observations: readonly string[];
  readonly observedAt: string;
  readonly receiptFingerprint: string;
}

export interface OwnerUiReceiptExpectation {
  readonly proofId: OwnerUiProofId;
  readonly seedFingerprint: string;
  readonly manifestStage: "seed" | "owner-created";
  readonly manifestGeneration: number;
  readonly entityIds: OwnerUiReceipt["entityIds"];
  readonly requiredObservations: readonly string[];
}

export const ownerAuthoringObservations = Object.freeze([
  "project-visible-after-reload",
  "vision-visible-after-reload",
  "task-dependencies-visible-after-reload",
  "dependent-tasks-blocked",
  "dependency-cycle-rejected",
]);

export const ownerApprovalObservations = Object.freeze([
  "factory-run-visible",
  "plan-approved-visible-after-reload",
  "first-task-causal-state-visible-after-reload",
]);

export function ownerUiReceiptPath(manifestPath: string, proofId: OwnerUiProofId): string {
  return `${manifestPath}.${proofId}.ui-receipt.json`;
}

function fingerprintPayload(receipt: Omit<OwnerUiReceipt, "receiptFingerprint">): string {
  return manifestFingerprint(receipt);
}

function stableIds(value: OwnerUiReceipt["entityIds"]): string {
  return manifestFingerprint(value);
}

export function persistOwnerUiReceipt(
  manifestPath: string,
  input: Omit<OwnerUiReceipt, "schemaVersion" | "observedAt" | "receiptFingerprint">,
): OwnerUiReceipt {
  const body = {
    schemaVersion: 1 as const,
    ...input,
    observedAt: new Date().toISOString(),
  };
  const receipt: OwnerUiReceipt = {
    ...body,
    receiptFingerprint: fingerprintPayload(body),
  };
  const path = ownerUiReceiptPath(manifestPath, input.proofId);
  const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
  const fd = openSync(temp, "wx", 0o600);
  try {
    writeFileSync(fd, `${JSON.stringify(receipt, null, 2)}\n`);
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
  renameSync(temp, path);
  const directoryFd = openSync(dirname(path), "r");
  try {
    fsyncSync(directoryFd);
  } finally {
    closeSync(directoryFd);
  }
  return receipt;
}

export function loadOwnerUiReceipt(
  manifestPath: string,
  expected: OwnerUiReceiptExpectation,
): OwnerUiReceipt | undefined {
  const path = ownerUiReceiptPath(manifestPath, expected.proofId);
  let parsed: unknown;
  try {
    parsed = JSON.parse(readFileSync(path, "utf8"));
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
    throw new Error(`cannot read owner UI receipt ${path}`, { cause: error });
  }
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
    throw new Error(`owner UI receipt ${path} is not an object`);
  const receipt = parsed as Partial<OwnerUiReceipt>;
  if (
    receipt.schemaVersion !== 1 ||
    receipt.proofId !== expected.proofId ||
    receipt.seedFingerprint !== expected.seedFingerprint ||
    receipt.manifestStage !== expected.manifestStage ||
    receipt.manifestGeneration !== expected.manifestGeneration ||
    !receipt.entityIds ||
    !Array.isArray(receipt.observations) ||
    typeof receipt.observedAt !== "string" ||
    typeof receipt.receiptFingerprint !== "string"
  ) {
    throw new Error(`owner UI receipt ${path} has invalid identity`);
  }
  const withoutFingerprint = {
    schemaVersion: receipt.schemaVersion,
    proofId: receipt.proofId,
    seedFingerprint: receipt.seedFingerprint,
    manifestStage: receipt.manifestStage,
    manifestGeneration: receipt.manifestGeneration,
    entityIds: receipt.entityIds,
    observations: receipt.observations,
    observedAt: receipt.observedAt,
  };
  if (fingerprintPayload(withoutFingerprint) !== receipt.receiptFingerprint)
    throw new Error(`owner UI receipt ${path} fingerprint mismatch`);
  if (stableIds(receipt.entityIds) !== stableIds(expected.entityIds))
    throw new Error(`owner UI receipt ${path} entity identity mismatch`);
  const observed = new Set(receipt.observations);
  if (expected.requiredObservations.some((item) => !observed.has(item)))
    throw new Error(`owner UI receipt ${path} is missing required observations`);
  return receipt as OwnerUiReceipt;
}

export function removeOwnerUiReceipt(manifestPath: string, proofId: OwnerUiProofId): void {
  rmSync(ownerUiReceiptPath(manifestPath, proofId), { force: true });
}
