import { existsSync, readFileSync, statSync } from "node:fs";
import { basename, join } from "node:path";
import { defaultRepoRoots } from "./landq";

/** One line of `.git/harness/landq/queue`: FIFO ticket ids, one per line. */
const TICKET_ID_RE = /^ticket\.[0-9a-f]{32}$/;

export interface LandqTicketState {
  ticket: string;
  branch: string | null;
  gateClass: string | null;
  ownerPid: number | null;
  enqueuedAt: string | null;
  waitSeconds: number | null;
  queueDepthAtArrival: number | null;
  position: number;
  /** Whether the ticket's own lock is currently held by a live process. `null` = could not tell. */
  ownerLockHeld: boolean | null;
  /** The `.job` file had a line the server would reject; still reported as waiting, never dropped. */
  malformed: boolean;
}

export interface LandqRepoState {
  repoRoot: string;
  project: string;
  status: "ok" | "absent" | "error";
  queueDepth: number;
  waiting: LandqTicketState[];
  waitingComplete: boolean;
  conductorHeld: boolean | null;
  conductorHolderPid: number | null;
  /** Seconds the holder process has been running. NOT lock-hold duration — /proc/locks carries no acquisition time. */
  conductorHolderAgeSeconds: number | null;
  conductorComplete: boolean;
}

export interface LandqStateResult {
  repos: LandqRepoState[];
}

export interface LandqStateOptions {
  repoRoots?: string[];
  readFileImpl?: (path: string) => string;
  existsSyncImpl?: (path: string) => boolean;
  statImpl?: (path: string) => { dev: number; ino: number };
  readProcLocksImpl?: () => string | null;
  readProcStatImpl?: (pid: number) => string | null;
  readProcUptimeImpl?: () => string | null;
  nowMs?: () => number;
}

function decodeB64(value: string): string | null {
  try {
    return Buffer.from(value, "base64").toString("utf8");
  } catch {
    return null;
  }
}

interface LockHolder {
  pid: number;
}

/** Read-only lock probe: matches `/proc/locks` rows by inode, never opens or acquires the file. */
function findLockHolder(
  path: string,
  statImpl: (path: string) => { dev: number; ino: number },
  readProcLocksImpl: () => string | null,
): LockHolder | null | undefined {
  let ino: number;
  try {
    ino = statImpl(path).ino;
  } catch {
    return null;
  }
  const raw = readProcLocksImpl();
  if (raw === null) return undefined;
  for (const line of raw.split("\n")) {
    const fields = line.trim().split(/\s+/);
    if (fields.length === 0 || fields[0] === "") continue;
    const waiter = fields[1] === "->";
    const base = waiter ? fields.slice(2) : fields.slice(1);
    // base: [POSIX|FLOCK, ADVISORY|MANDATORY, READ|WRITE, pid, dev:dev:inode, start, end]
    if (base.length < 5) continue;
    if (waiter) continue; // a blocked waiter does not hold the lock
    const pidStr = base[3];
    const devInode = base[4];
    const parts = devInode?.split(":") ?? [];
    const lockIno = parts.length === 3 ? Number(parts[2]) : NaN;
    if (!Number.isFinite(lockIno) || lockIno !== ino) continue;
    const pid = Number(pidStr);
    if (!Number.isFinite(pid) || pid <= 0) continue;
    return { pid };
  }
  return null;
}

const LINUX_USER_HZ = 100;

function holderAgeSeconds(
  pid: number,
  readProcStatImpl: (pid: number) => string | null,
  readProcUptimeImpl: () => string | null,
): number | null {
  const statLine = readProcStatImpl(pid);
  const uptimeLine = readProcUptimeImpl();
  if (statLine === null || uptimeLine === null) return null;
  const closeParen = statLine.lastIndexOf(")");
  if (closeParen === -1) return null;
  const rest = statLine.slice(closeParen + 2).trim().split(/\s+/);
  // rest[0] is field 3 (state); starttime is field 22, i.e. rest[19] (0-indexed from field 3).
  const starttimeTicks = Number(rest[19]);
  const uptimeSeconds = Number(uptimeLine.trim().split(/\s+/)[0]);
  if (!Number.isFinite(starttimeTicks) || !Number.isFinite(uptimeSeconds)) return null;
  const age = uptimeSeconds - starttimeTicks / LINUX_USER_HZ;
  return age >= 0 ? age : null;
}

function readTicketJob(
  dir: string,
  ticket: string,
  readFileImpl: (path: string) => string,
  existsSyncImpl: (path: string) => boolean,
): { fields: Record<string, string>; malformed: boolean } | null {
  const jobPath = join(dir, `${ticket}.job`);
  if (!existsSyncImpl(jobPath)) return null;
  let raw: string;
  try {
    raw = readFileImpl(jobPath);
  } catch {
    return { fields: {}, malformed: true };
  }
  const fields: Record<string, string> = {};
  let malformed = false;
  for (const rawLine of raw.split("\n")) {
    // Do not trim the whole line first: a field with an empty value (e.g. `assets_ok `)
    // has a trailing space that trim() would strip, hiding the key/value separator.
    const line = rawLine.replace(/\r$/, "");
    if (line === "") continue;
    const spaceIdx = line.indexOf(" ");
    if (spaceIdx === -1) {
      malformed = true;
      continue;
    }
    const key = line.slice(0, spaceIdx);
    const encoded = line.slice(spaceIdx + 1);
    const decoded = decodeB64(encoded);
    if (decoded === null) {
      malformed = true;
      continue;
    }
    fields[key] = decoded;
  }
  return { fields, malformed };
}

function readRepoState(
  repoRoot: string,
  readFileImpl: (path: string) => string,
  existsSyncImpl: (path: string) => boolean,
  statImpl: (path: string) => { dev: number; ino: number },
  readProcLocksImpl: () => string | null,
  readProcStatImpl: (pid: number) => string | null,
  readProcUptimeImpl: () => string | null,
  nowMs: number,
): LandqRepoState {
  const dir = join(repoRoot, ".git", "harness", "landq");
  const queuePath = join(dir, "queue");
  const project = basename(repoRoot);

  if (!existsSyncImpl(dir)) {
    return {
      repoRoot,
      project,
      status: "absent",
      queueDepth: 0,
      waiting: [],
      waitingComplete: true,
      conductorHeld: null,
      conductorHolderPid: null,
      conductorHolderAgeSeconds: null,
      conductorComplete: true,
    };
  }

  let waitingComplete = true;
  const waiting: LandqTicketState[] = [];

  if (existsSyncImpl(queuePath)) {
    let raw: string;
    try {
      raw = readFileImpl(queuePath);
    } catch {
      waitingComplete = false;
      raw = "";
    }
    let position = 0;
    for (const line of raw.split("\n")) {
      const ticket = line.trim();
      if (!ticket) continue;
      if (!TICKET_ID_RE.test(ticket)) {
        waitingComplete = false;
        continue;
      }
      position += 1;
      const verdictPath = join(dir, `${ticket}.verdict`);
      if (existsSyncImpl(verdictPath)) continue; // resolved, not waiting

      const job = readTicketJob(dir, ticket, readFileImpl, existsSyncImpl);
      const ticketLockPath = join(dir, `${ticket}.lock`);
      const holder = findLockHolder(ticketLockPath, statImpl, readProcLocksImpl);
      const ownerLockHeld = holder === undefined ? null : holder !== null;
      if (holder === undefined) waitingComplete = false;

      const enqueuedAt = job?.fields.enqueued_at ?? null;
      const enqueuedMs = enqueuedAt ? Date.parse(enqueuedAt) : NaN;
      const waitSeconds = Number.isFinite(enqueuedMs) ? Math.max(0, Math.round((nowMs - enqueuedMs) / 1000)) : null;
      const depthAtArrival = job?.fields.queue_depth ? Number(job.fields.queue_depth) : null;

      waiting.push({
        ticket,
        branch: job?.fields.branch ?? null,
        gateClass: job?.fields.gate_class ?? null,
        ownerPid: job?.fields.owner_pid ? Number(job.fields.owner_pid) : null,
        enqueuedAt,
        waitSeconds,
        queueDepthAtArrival: Number.isFinite(depthAtArrival) ? depthAtArrival : null,
        position,
        ownerLockHeld,
        malformed: job === null || job.malformed,
      });
    }
  } else {
    waitingComplete = false;
  }

  const conductorLockPath = join(dir, "conductor.lock");
  let conductorHeld: boolean | null = null;
  let conductorHolderPid: number | null = null;
  let conductorHolderAgeSeconds: number | null = null;
  let conductorComplete = true;
  if (existsSyncImpl(conductorLockPath)) {
    const holder = findLockHolder(conductorLockPath, statImpl, readProcLocksImpl);
    if (holder === undefined) {
      conductorComplete = false;
    } else {
      conductorHeld = holder !== null;
      conductorHolderPid = holder?.pid ?? null;
      if (holder) {
        conductorHolderAgeSeconds = holderAgeSeconds(holder.pid, readProcStatImpl, readProcUptimeImpl);
      }
    }
  } else {
    conductorHeld = false;
  }

  return {
    repoRoot,
    project,
    status: "ok",
    queueDepth: waiting.length,
    waiting,
    waitingComplete,
    conductorHeld,
    conductorHolderPid,
    conductorHolderAgeSeconds,
    conductorComplete,
  };
}

export function readLandqState(options: LandqStateOptions = {}): LandqStateResult {
  const {
    repoRoots = defaultRepoRoots(),
    readFileImpl = (path) => readFileSync(path, "utf8"),
    existsSyncImpl = existsSync,
    statImpl = (path) => statSync(path),
    readProcLocksImpl = () => {
      try {
        return readFileSync("/proc/locks", "utf8");
      } catch {
        return null;
      }
    },
    readProcStatImpl = (pid) => {
      try {
        return readFileSync(`/proc/${pid}/stat`, "utf8");
      } catch {
        return null;
      }
    },
    readProcUptimeImpl = () => {
      try {
        return readFileSync("/proc/uptime", "utf8");
      } catch {
        return null;
      }
    },
    nowMs = () => Date.now(),
  } = options;

  const repos = repoRoots.map((repoRoot) =>
    readRepoState(
      repoRoot,
      readFileImpl,
      existsSyncImpl,
      statImpl,
      readProcLocksImpl,
      readProcStatImpl,
      readProcUptimeImpl,
      nowMs(),
    ),
  );

  return { repos };
}
