import { createHash, randomUUID } from "node:crypto";
import {
  closeSync,
  mkdirSync,
  openSync,
  readFileSync,
  renameSync,
  unlinkSync,
  writeFileSync,
} from "node:fs";
import { join } from "node:path";

export interface RecoveryFence {
  token: string;
  path: string;
  task_id: string;
  agent_id: string;
  attempt_id: string | null;
  lease_id: string | null;
  fingerprint: string;
  acquired_at: string;
  expires_at: string;
}

function fencePath(projectRoot: string, taskId: string, agentId: string): string {
  const digest = createHash("sha256")
    .update(`${taskId.replace(/\.md$/i, "").trim().toUpperCase()}:${agentId.trim()}`)
    .digest("hex")
    .slice(0, 24);
  return join(projectRoot, ".codeflowmu", "pm-governance", "recovery-fences", `${digest}.json`);
}

function readFence(path: string): RecoveryFence | null {
  try {
    return JSON.parse(readFileSync(path, "utf-8")) as RecoveryFence;
  } catch {
    return null;
  }
}

/** Cross-process exclusive recovery fence, bound to the observed attempt/lease snapshot. */
export function tryAcquireRecoveryFence(input: {
  projectRoot: string;
  taskId: string;
  agentId: string;
  attemptId?: string | null;
  leaseId?: string | null;
  now?: Date;
  ttlMs?: number;
}): RecoveryFence | null {
  const now = input.now ?? new Date();
  const path = fencePath(input.projectRoot, input.taskId, input.agentId);
  mkdirSync(join(input.projectRoot, ".codeflowmu", "pm-governance", "recovery-fences"), {
    recursive: true,
  });

  const attemptId = String(input.attemptId ?? "").trim() || null;
  const leaseId = String(input.leaseId ?? "").trim() || null;
  const fingerprint = createHash("sha256")
    .update(`${input.taskId}:${input.agentId}:${attemptId ?? "none"}:${leaseId ?? "none"}`)
    .digest("hex");
  const fence: RecoveryFence = {
    token: randomUUID(),
    path,
    task_id: input.taskId,
    agent_id: input.agentId,
    attempt_id: attemptId,
    lease_id: leaseId,
    fingerprint,
    acquired_at: now.toISOString(),
    expires_at: new Date(now.getTime() + Math.max(10_000, input.ttlMs ?? 120_000)).toISOString(),
  };

  const acquire = (): RecoveryFence | null => {
    let fd: number | null = null;
    try {
      fd = openSync(path, "wx");
      writeFileSync(fd, `${JSON.stringify(fence, null, 2)}\n`, "utf-8");
      return fence;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
      return null;
    } finally {
      if (fd != null) closeSync(fd);
    }
  };

  const first = acquire();
  if (first) return first;
  const existing = readFence(path);
  if (!existing || Date.parse(existing.expires_at) <= now.getTime()) {
    const stalePath = `${path}.stale-${process.pid}-${randomUUID()}`;
    try {
      // Rename, rather than unlink, makes stale takeover race-safe: only one
      // contender can move the observed file and no contender can delete a
      // freshly acquired fence from another process.
      renameSync(path, stalePath);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === "ENOENT") return acquire();
      return null;
    }
    try {
      unlinkSync(stalePath);
    } catch {
      /* stale quarantine cleanup is best-effort */
    }
    return acquire();
  }
  return null;
}

export function releaseRecoveryFence(fence: RecoveryFence): boolean {
  const current = readFence(fence.path);
  if (!current || current.token !== fence.token) return false;
  try {
    unlinkSync(fence.path);
    return true;
  } catch {
    return false;
  }
}
