import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import { spawn as spawnAsync, spawnSync } from "node:child_process";
import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { z } from "zod";
import type { ControllerStore, LandOperationRecord, LandSuccessorDispatch } from "./store";

const TicketSchema = z.string().regex(/^ticket\.[0-9a-f]{32}$/);
const AbsolutePathSchema = z.string().min(1).refine((value) => value.startsWith("/"));
const ReceiptSchema = z.object({
  requestId: z.string().min(1),
  taskId: z.string().min(1),
  root: AbsolutePathSchema,
  worktree: AbsolutePathSchema,
  ref: z.string().min(1),
  commit: z.string().regex(/^[0-9a-f]{40}$/),
  artifact: z.string().min(1),
  verification: z.string().min(1),
  pendingOperation: z.literal("land"),
  blocker: z.string(),
  nextAction: z.string().min(1),
  rollback: z.string().min(1),
  successorAccount: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/),
  successorModel: z.string().min(1),
  queueDir: AbsolutePathSchema,
  ticketId: TicketSchema,
}).strict();

export const LandOperationRequestSchema = z.object({
  operationId: z.string().regex(/^[A-Za-z0-9._:-]{1,160}$/),
  receipt: ReceiptSchema,
  leaseTtlMs: z.number().int().min(1_000).max(15 * 60_000).default(60_000),
}).strict();

export type LandOperationRequest = z.infer<typeof LandOperationRequestSchema>;

export interface LandRetirementOptions {
  now?: () => number;
  conductor?: (operation: LandOperationRecord) => void;
  dispatch?: (intent: LandSuccessorDispatch) => SeatDispatchAcceptance | null;
  conductorScript?: string;
  repositoryRoots?: string[];
  spawn?: typeof spawnSync;
  conductRoot?: (root: string) => { ok: boolean; detail: string } | Promise<{ ok: boolean; detail: string }>;
}

export interface SeatDispatchAcceptance {
  identity: string;
  generation: number;
  state: string;
}

export interface SeatDispatchResult {
  accepted: SeatDispatchAcceptance;
}

export class LandRetirementService {
  private readonly now: () => number;
  private readonly conductor: (operation: LandOperationRecord) => void;
  private readonly dispatch: (intent: LandSuccessorDispatch) => SeatDispatchAcceptance | null;
  private readonly resolvePaths: (operation: LandOperationRecord) => TrustedLandPaths;
  private readonly repositoryRoots: string[];
  private readonly conductRoot: (root: string) => { ok: boolean; detail: string } | Promise<{ ok: boolean; detail: string }>;
  private conductRunning = false;

  constructor(private readonly store: ControllerStore, options: LandRetirementOptions = {}) {
    this.now = options.now ?? (() => Date.now());
    this.repositoryRoots = options.repositoryRoots ?? [];
    const conductorScript = options.conductorScript ?? join(process.cwd(), "..", "modules", "workstation", "claude", "workflows", "lib", "finish-branch.sh");
    const spawn = options.spawn ?? spawnSync;
    this.conductRoot = options.conductRoot ?? ((candidate) => new Promise((resolvePromise) => {
      let allowedRoots: string[];
      let root: string;
      try {
        allowedRoots = this.repositoryRoots.map((configuredRoot) => resolve(configuredRoot));
        root = trustedRepositoryRoot(candidate, allowedRoots);
      } catch (error) {
        resolvePromise({ ok: false, detail: (error as Error).message });
        return;
      }
      // A conduct pass can legitimately run for hours draining a large backlog. It must not die
      // when the controller service itself restarts (deploy, crash-restart, manual bounce):
      // systemd's default KillMode=control-group SIGKILLs every PID in the controller's cgroup on
      // stop, and a plain child process — detached or not — stays in that cgroup. Launching it as
      // its own transient systemd scope (`systemd-run --scope`) puts it in a separate cgroup the
      // controller's restart cannot reach, so the ticket-level flocks and the conductor's own
      // non-blocking `flock -n` (a fresh controller instance skips cleanly while this one runs) are
      // what keep it safe, per the plan's "a ticket is the complete job description; a conductor
      // resolves nothing from its own environment."
      const child = spawnAsync("systemd-run", [
        "--user", "--scope", "--collect", "--quiet",
        `--unit=landq-conduct-${process.pid}-${Date.now()}`,
        "--", "bash", conductorScript, "landq-conduct", root,
      ], {
        stdio: "ignore",
        env: { ...process.env, FINISH_BRANCH_CONTROLLER_RETIRE: "1" },
      });
      child.once("error", (error) => resolvePromise({ ok: false, detail: error.message }));
      child.once("exit", (code) => {
        if (code === 0) resolvePromise({ ok: true, detail: "" });
        else resolvePromise({ ok: false, detail: `rc=${code ?? "unknown"}` });
      });
    }));
    if (options.conductor) {
      this.resolvePaths = (operation) => ({ root: operation.receipt.root, queueDir: operation.receipt.queueDir });
      this.conductor = options.conductor;
    } else {
      const configured = configureLandConductor({
        conductorScript,
        repositoryRoots: this.repositoryRoots,
        spawn,
      });
      this.resolvePaths = configured.resolve;
      this.conductor = configured.start;
    }
    this.dispatch = options.dispatch ?? dispatchSuccessorThroughSeat;
  }

  accept(input: LandOperationRequest): LandOperationRecord {
    const parsed = LandOperationRequestSchema.parse(input);
    const operation = this.store.acceptLandOperation({
      operationId: parsed.operationId,
      receipt: parsed.receipt,
      leaseExpiresAt: this.now() + parsed.leaseTtlMs,
    });
    const paths = this.resolvePaths(operation);
    ensureControllerTicketClaim(operation, paths.queueDir);
    this.conductor(operation);
    return operation;
  }

  reconcile(): void {
    for (const operation of this.store.listLandOperationsAwaitingVerdict()) {
      let paths: TrustedLandPaths;
      try {
        paths = this.resolvePaths(operation);
      } catch {
        continue;
      }
      const verdict = readLandQueueVerdict(operation, paths.queueDir);
      if (verdict) {
        this.store.recordLandOperationVerdict({
          operationId: operation.operationId,
          ticketId: operation.receipt.ticketId,
          fence: operation.fence,
          verdict,
        });
      } else {
        const renewed = this.store.recoverExpiredLandOperationLease(operation.operationId, this.now() + 60_000);
        if (renewed) {
          const renewedPaths = this.resolvePaths(renewed);
          ensureControllerTicketClaim(renewed, renewedPaths.queueDir);
          this.conductor(renewed);
        }
      }
    }
    for (const intent of this.store.claimPendingLandSuccessorDispatches()) {
      const accepted = this.dispatch(intent);
      if (accepted && accepted.state === "running" && Number.isSafeInteger(accepted.generation) && accepted.generation > 0) {
        this.store.markLandSuccessorDispatched(intent);
      }
    }
  }

  async conductConfiguredRoots(log: (line: string) => void = (line) => process.stderr.write(`${line}\n`)): Promise<void> {
    if (this.conductRunning) return;
    this.conductRunning = true;
    try {
      for (const root of this.repositoryRoots) {
        let result: { ok: boolean; detail: string };
        try {
          result = await this.conductRoot(root);
        } catch (error) {
          const detail = (error as Error).message;
          log(`land-conduct: skipping ${root}: ${detail}`);
          this.store.recordLandConductResult(root, new Date(this.now()).toISOString(), false, detail);
          continue;
        }
        if (!result.ok) log(`land-conduct: ${root} conduct pass failed (${result.detail})`);
        this.store.recordLandConductResult(root, new Date(this.now()).toISOString(), result.ok, result.detail);
      }
    } finally {
      this.conductRunning = false;
    }
  }
}

function ensureControllerTicketClaim(operation: LandOperationRecord, queueDir: string): void {
  const claim = `${operation.operationId}\n${operation.fence}\n`;
  const path = join(queueDir, `${operation.receipt.ticketId}.controller`);
  if (existsSync(path)) {
    const existing = readFileSync(path, "utf8");
    const [operationId, fence] = existing.split("\n");
    if (operationId !== operation.operationId || !/^[1-9]\d*$/.test(fence ?? "")) throw new Error("land ticket controller claim mismatch");
    if (Number(fence) > operation.fence) throw new Error("land ticket controller claim fence is newer");
    if (Number(fence) === operation.fence) return;
    writeFileSync(path, claim, { mode: 0o600 });
    return;
  }
  try {
    writeFileSync(path, claim, { mode: 0o600, flag: "wx" });
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
    if (!existsSync(path)) throw error;
    if (readFileSync(path, "utf8") !== claim) throw new Error("land ticket controller claim mismatch");
  }
}

function readLandQueueVerdict(operation: LandOperationRecord, queueDir: string): unknown | null {
  const path = join(queueDir, `${operation.receipt.ticketId}.verdict`);
  if (!existsSync(path)) return null;
  const [rc, ...output] = readFileSync(path, "utf8").split("\n");
  if (!/^\d+$/.test(rc ?? "")) throw new Error("land queue verdict is malformed");
  const body = output.join("\n").trim();
  if (!body) throw new Error("land queue verdict has no result");
  const parsed = JSON.parse(body) as { status?: unknown };
  if (typeof parsed.status !== "string" || !parsed.status) throw new Error("land queue verdict has no status");
  return { rc: Number(rc), result: parsed };
}

interface TrustedLandPaths {
  root: string;
  queueDir: string;
}

interface LandConductorConfig {
  conductorScript: string;
  repositoryRoots: string[];
  spawn: typeof spawnSync;
}

function configureLandConductor(config: LandConductorConfig): {
  resolve: (operation: LandOperationRecord) => TrustedLandPaths;
  start: (operation: LandOperationRecord) => void;
} {
  const script = trustedRegularFile(config.conductorScript, "land conductor script");
  const allowedRoots = config.repositoryRoots.map((root) => {
    if (!isAbsolute(root)) throw new Error("repository allowlist root must be absolute");
    return resolve(root);
  });
  const resolvePaths = (operation: LandOperationRecord) => {
    const root = trustedRepositoryRoot(operation.receipt.root, allowedRoots);
    const queueDir = trustedQueueDir(root, allowedRoots);
    if (realpathSync(operation.receipt.queueDir) !== queueDir) throw new Error("land queue directory does not match repository");
    rejectSymlinkComponents(operation.receipt.queueDir, "land queue directory");
    return { root, queueDir };
  };
  return {
    resolve: resolvePaths,
    start: (operation) => {
      const { root } = resolvePaths(operation);
      const child = config.spawn("bash", [script, "landq-conduct", root], {
        stdio: "ignore",
        env: { ...process.env, FINISH_BRANCH_CONTROLLER_RETIRE: "1" },
      });
      if (child.error) throw child.error;
    },
  };
}

function trustedRepositoryRoot(candidate: string, allowedRoots: string[]): string {
  const root = trustedDirectory(candidate, "repository root");
  if (!allowedRoots.includes(root)) throw new Error("repository root is not configured");
  return root;
}

function trustedQueueDir(root: string, allowedRoots: string[]): string {
  const gitEntry = join(root, ".git");
  if (lstatSync(gitEntry).isSymbolicLink()) throw new Error("repository git metadata must not be a symlink");
  let commonDir: string;
  if (lstatSync(gitEntry).isDirectory()) {
    commonDir = gitEntry;
  } else {
    const match = /^gitdir: (.+)\n?$/.exec(readFileSync(gitEntry, "utf8"));
    if (!match) throw new Error("repository git metadata is malformed");
    commonDir = resolve(root, match[1]!);
    const commonDirFile = join(commonDir, "commondir");
    if (existsSync(commonDirFile)) commonDir = resolve(commonDir, readFileSync(commonDirFile, "utf8").trim());
  }
  const canonicalCommonDir = trustedDirectory(commonDir, "repository common git directory");
  if (!allowedRoots.some((allowed) => isWithin(allowed, canonicalCommonDir))) throw new Error("repository git metadata escapes configured roots");
  return trustedDirectory(join(canonicalCommonDir, "harness", "landq"), "land queue directory");
}

function trustedDirectory(path: string, label: string): string {
  if (!isAbsolute(path)) throw new Error(`${label} must be absolute`);
  rejectSymlinkComponents(path, label);
  const canonical = realpathSync(path);
  if (!lstatSync(canonical).isDirectory()) throw new Error(`${label} must be a directory`);
  return canonical;
}

function trustedRegularFile(path: string, label: string): string {
  if (!isAbsolute(path)) throw new Error(`${label} must be absolute`);
  rejectSymlinkComponents(path, label);
  const canonical = realpathSync(path);
  if (!lstatSync(canonical).isFile()) throw new Error(`${label} must be a file`);
  return canonical;
}

function rejectSymlinkComponents(path: string, label: string): void {
  let current: string = sep;
  for (const component of resolve(path).split(sep).filter(Boolean)) {
    current = join(current, component);
    if (lstatSync(current).isSymbolicLink()) throw new Error(`${label} must not contain symlinks`);
  }
}

function isWithin(parent: string, child: string): boolean {
  const path = relative(parent, child);
  return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
}

function dispatchSuccessorThroughSeat(intent: LandSuccessorDispatch): SeatDispatchAcceptance | null {
  const child = spawnSync("seat-remote", successorSeatArgs(intent), {
    encoding: "utf8",
    stdio: ["ignore", "pipe", "ignore"],
  });
  if (child.error || child.status !== 0) return null;
  return parseSeatDispatchAcceptance(child.stdout);
}

function parseSeatDispatchAcceptance(output: string | Buffer | null): SeatDispatchAcceptance | null {
  try {
    const parsed = JSON.parse(String(output ?? "")) as Partial<SeatDispatchResult>;
    const accepted = parsed.accepted;
    if (!accepted || typeof accepted.identity !== "string" || !accepted.identity || !Number.isSafeInteger(accepted.generation) || typeof accepted.state !== "string") return null;
    return accepted;
  } catch {
    return null;
  }
}

export function successorSeatArgs(intent: LandSuccessorDispatch): string[] {
  return [
    "launch",
    "--host", "auto",
    "--account", intent.receipt.successorAccount,
    "--project", intent.receipt.worktree,
    "--model", intent.receipt.successorModel,
    "--dispatch-key", intent.dispatchKey,
    "--",
    "-p", `Resume land operation ${intent.operationId} at fence ${intent.fence} with dispatch key ${intent.dispatchKey}. Reconstruct durable receipt from controller state, then ${intent.nextAction}.`,
  ];
}
