import type { RunRecord, RunState, TerminationReason } from "./types.js";

function nowMs() {
  return Date.now();
}

const DEFAULT_MAX_EXITED_RECORDS = 2_000;

function resolveMaxExitedRecords(value?: number): number {
  if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
    return DEFAULT_MAX_EXITED_RECORDS;
  }
  return Math.max(1, Math.floor(value));
}

export type RunRegistry = {
  add: (record: RunRecord) => void;
  get: (runId: string) => RunRecord | undefined;
  list: () => RunRecord[];
  listByScope: (scopeKey: string) => RunRecord[];
  updateState: (
    runId: string,
    state: RunState,
    patch?: Partial<Pick<RunRecord, "pid" | "terminationReason" | "exitCode" | "exitSignal">>,
  ) => RunRecord | undefined;
  touchOutput: (runId: string) => void;
  finalize: (
    runId: string,
    exit: {
      reason: TerminationReason;
      exitCode: number | null;
      exitSignal: NodeJS.Signals | number | null;
    },
  ) => { record: RunRecord; firstFinalize: boolean } | null;
  delete: (runId: string) => void;
};

export function createRunRegistry(options?: { maxExitedRecords?: number }): RunRegistry {
  const records = new Map<string, RunRecord>();
  const maxExitedRecords = resolveMaxExitedRecords(options?.maxExitedRecords);

  const pruneExitedRecords = () => {
    if (!records.size) {
      return;
    }
    let exited = 0;
    for (const record of records.values()) {
      if (record.state === "exited") {
        exited += 1;
      }
    }
    if (exited <= maxExitedRecords) {
      return;
    }
    let remove = exited - maxExitedRecords;
    for (const [runId, record] of records.entries()) {
      if (remove <= 0) {
        break;
      }
      if (record.state !== "exited") {
        continue;
      }
      records.delete(runId);
      remove -= 1;
    }
  };

  const add: RunRegistry["add"] = (record) => {
    records.set(record.runId, { ...record });
  };

  const get: RunRegistry["get"] = (runId) => {
    const record = records.get(runId);
    return record ? { ...record } : undefined;
  };

  const list: RunRegistry["list"] = () => {
    return Array.from(records.values()).map((record) => ({ ...record }));
  };

  const listByScope: RunRegistry["listByScope"] = (scopeKey) => {
    if (!scopeKey.trim()) {
      return [];
    }
    return Array.from(records.values())
      .filter((record) => record.scopeKey === scopeKey)
      .map((record) => ({ ...record }));
  };

  const updateState: RunRegistry["updateState"] = (runId, state, patch) => {
    const current = records.get(runId);
    if (!current) {
      return undefined;
    }
    const updatedAtMs = nowMs();
    const next: RunRecord = {
      ...current,
      ...patch,
      state,
      updatedAtMs,
      lastOutputAtMs: current.lastOutputAtMs,
    };
    records.set(runId, next);
    return { ...next };
  };

  const touchOutput: RunRegistry["touchOutput"] = (runId) => {
    const current = records.get(runId);
    if (!current) {
      return;
    }
    const ts = nowMs();
    records.set(runId, {
      ...current,
      lastOutputAtMs: ts,
      updatedAtMs: ts,
    });
  };

  const finalize: RunRegistry["finalize"] = (runId, exit) => {
    const current = records.get(runId);
    if (!current) {
      return null;
    }
    const firstFinalize = current.state !== "exited";
    const ts = nowMs();
    const next: RunRecord = {
      ...current,
      state: "exited",
      terminationReason: current.terminationReason ?? exit.reason,
      exitCode: current.exitCode !== undefined ? current.exitCode : exit.exitCode,
      exitSignal: current.exitSignal !== undefined ? current.exitSignal : exit.exitSignal,
      updatedAtMs: ts,
    };
    records.set(runId, next);
    pruneExitedRecords();
    return { record: { ...next }, firstFinalize };
  };

  const del: RunRegistry["delete"] = (runId) => {
    records.delete(runId);
  };

  return {
    add,
    get,
    list,
    listByScope,
    updateState,
    touchOutput,
    finalize,
    delete: del,
  };
}
