import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { stateDir } from "./paths";

export interface StoredGhRun {
  id: number;
  run_attempt: number;
  run_number: number;
  name: string;
  head_branch: string;
  head_sha: string;
  display_title: string;
  status: string;
  conclusion: string | null;
  workflow_id: number;
  html_url: string;
  created_at: string;
  updated_at: string;
}

export interface RepoRunHistory {
  runs: StoredGhRun[];
  lastSyncedAt: string | null;
}

export type RunHistoryStore = Record<string, RepoRunHistory>;

export const MAX_STORED_RUNS_PER_REPO = 500;

const warnedPaths = new Set<string>();

export function runHistoryFile(): string {
  return join(stateDir(), "ghci-run-history.json");
}

function warnCorruptOnce(path: string, error: unknown): void {
  if (warnedPaths.has(path)) return;
  warnedPaths.add(path);
  const message = error instanceof Error ? error.message : String(error);
  console.warn(`[ghci-run-history] ignored corrupt ${path}: ${message}`);
}

function isStoredRun(value: unknown): value is StoredGhRun {
  if (!value || typeof value !== "object") return false;
  const run = value as Record<string, unknown>;
  return Number.isSafeInteger(run.id)
    && Number.isSafeInteger(run.run_attempt)
    && typeof run.name === "string"
    && typeof run.updated_at === "string";
}

function isRunHistoryStore(value: unknown): value is RunHistoryStore {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  return Object.values(value).every((entry) => {
    if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
    const repo = entry as RepoRunHistory;
    return Array.isArray(repo.runs)
      && repo.runs.every(isStoredRun)
      && (repo.lastSyncedAt === null || typeof repo.lastSyncedAt === "string");
  });
}

export function readRunHistory(path: string = runHistoryFile()): RunHistoryStore {
  try {
    const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
    if (!isRunHistoryStore(parsed)) throw new Error("invalid run history shape");
    return parsed;
  } catch (error) {
    if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
    warnCorruptOnce(path, error);
    return {};
  }
}

export function writeRunHistory(store: RunHistoryStore, path: string = runHistoryFile()): void {
  const dir = stateDir();
  const temporaryPath = `${path}.${process.pid}.tmp`;
  mkdirSync(dir, { recursive: true, mode: 0o700 });
  writeFileSync(temporaryPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
  renameSync(temporaryPath, path);
}

function runKey(run: StoredGhRun): string {
  return `${run.id}:${run.run_attempt}`;
}

function compareRuns(left: StoredGhRun, right: StoredGhRun): number {
  const updated = Date.parse(right.updated_at) - Date.parse(left.updated_at);
  if (updated !== 0) return updated;
  return right.id - left.id;
}

export function mergeRepoRuns(
  store: RunHistoryStore,
  repo: string,
  incoming: StoredGhRun[],
  syncedAt: string,
): StoredGhRun[] {
  const current = store[repo] ?? { runs: [], lastSyncedAt: null };
  const byKey = new Map(current.runs.map((run) => [runKey(run), run]));
  for (const run of incoming) byKey.set(runKey(run), run);
  const merged = [...byKey.values()].sort(compareRuns).slice(0, MAX_STORED_RUNS_PER_REPO);
  store[repo] = { runs: merged, lastSyncedAt: syncedAt };
  return merged;
}

export function recentRepoRuns(
  store: RunHistoryStore,
  repo: string,
  limit: number,
): StoredGhRun[] {
  return (store[repo]?.runs ?? []).slice(0, limit);
}
