/**
 * ProjectAgentLock — ADR-0002 invariant enforcement at the DB layer.
 *
 * `projectId @id` gives at-most-one lock per project; `runId @unique` gives a
 * run holds at most one lock. Those are DB-level. The spec's `WellFormed`
 * invariant ("holder is a non-terminal run") is app-level: it must hold on
 * every read of the lock table. `assertLockHolderActive` enforces it on the
 * read path — if a lock exists but its run is terminal, that's a bug (the run
 * should have released it), surfaced as an error rather than silently trusted.
 */
import type { PrismaClient } from "@prisma/client";
import type { AgentRunStatus } from "@prisma/client";

/// spec RunState.Terminal: completed/failed/timedOut/canceled.
/// active/waitingForUser are NOT terminal — the lock must still be held.
const TERMINAL_STATUSES: ReadonlySet<AgentRunStatus> = new Set([
  "COMPLETED",
  "FAILED",
  "TIMED_OUT",
  "CANCELED",
]);

export function isTerminal(status: AgentRunStatus): boolean {
  return TERMINAL_STATUSES.has(status);
}

/**
 * Acquire the project lock for a run. Throws if the project is already locked
 * by another run (ADR-0002 exclusivity). The unique runId constraint means a
 * run that already holds a lock elsewhere will also fail — that's correct, a
 * run scopes to one project.
 */
export async function acquireLock(
  prisma: PrismaClient,
  projectId: string,
  runId: string,
  holderUserId: string | null,
): Promise<void> {
  await prisma.projectAgentLock.create({
    data: { projectId, runId, holderUserId },
  });
}

/**
 * Release the lock owned by a run. Idempotent — a no-op if no lock exists.
 * Called on run completion/failure/timeout/cancellation (ADR-0002).
 */
export async function releaseLock(prisma: PrismaClient, runId: string): Promise<void> {
  await prisma.projectAgentLock.deleteMany({ where: { runId } });
}

/**
 * The spec's `LockTable.WellFormed` on a single read: if `projectId` has a
 * lock owned by `runId`, that run must be non-terminal. Throws if a terminal
 * run still holds a lock — that's a release-path bug, not a recoverable state.
 *
 * Returns the lock's runId if a healthy lock exists, or null if unlocked.
 */
export async function currentLockRunId(
  prisma: PrismaClient,
  projectId: string,
): Promise<string | null> {
  const lock = await prisma.projectAgentLock.findUnique({
    where: { projectId },
    select: { runId: true },
  });
  if (lock === null) return null;

  const run = await prisma.agentRun.findUnique({
    where: { id: lock.runId },
    select: { status: true },
  });
  if (run !== null && isTerminal(run.status)) {
    throw new Error(
      `lock invariant violated: project ${projectId} locked by terminal run ${lock.runId} (status ${run.status}) — release path bug (ADR-0002 WellFormed)`,
    );
  }
  return lock.runId;
}
