import { spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { constants } from 'node:fs';
import { mkdir, open, rename } from 'node:fs/promises';
import { join } from 'node:path';
import type { Command } from 'commander';
import {
  TmuxPromotionTransport,
  type PromotionTarget,
  type PromotionTransport,
} from '../fleet/promotion-transport.js';

export type { PromotionTransport } from '../fleet/promotion-transport.js';
import { resolveFleetPaths, type CommandRunner } from './fleet.js';

const ATTEMPT_ID_PATTERN = /^[a-f0-9]{64}$/;
const DEFAULT_POLL_INTERVAL_MS = 250;
const DEFAULT_TIMEOUT_MS = 30_000;
const SUBPROCESS_TIMEOUT_MS = 4_500;
const PENDING_DIRECTORY = 'mosaic-lease';
const AUTHORIZATION_DIRECTORY = 'authorizations';
const AUTHORIZATION_TTL_SECONDS = 60;
const RESULT_FILE = 'last-result.json';

export interface PromotionBreadcrumb {
  attempt_id: string;
  expires_at_wallclock: number | null;
  reason: string | null;
  session_id: string;
  ts: number;
  verified: boolean;
}

export interface PromotionBreadcrumbStore {
  readAttemptId(sessionId: string): Promise<string | null>;
  readResult(): Promise<PromotionBreadcrumb | null>;
}

export interface PromotionResult {
  bundle: string;
  expiresAtWallclock: number | null;
  reason: string | null;
  seat: string;
  sessionId: string;
  status: 'VERIFIED' | 'UNVERIFIED';
}

export interface PromoteSeatOptions {
  clock?: () => number;
  pollIntervalMs?: number;
  sleep?: (milliseconds: number) => Promise<void>;
  store: PromotionBreadcrumbStore;
  target?: PromotionTarget;
  timeoutMs?: number;
  transport: PromotionTransport;
}

export interface PromoteCommandDeps {
  mintAuthorization?: (target: PromotionTarget) => Promise<void>;
  mosaicHome?: string;
  runner?: CommandRunner;
  store?: PromotionBreadcrumbStore;
  transport?: PromotionTransport;
}

/** Private, local result store shared with the in-seat completion hook. */
export class FilePromotionBreadcrumbStore implements PromotionBreadcrumbStore {
  constructor(private readonly runtimeDirectory = defaultRuntimeDirectory()) {}

  async readAttemptId(sessionId: string): Promise<string | null> {
    if (!ATTEMPT_ID_PATTERN.test(sessionId)) return null;
    const content = await readPrivateFile(
      join(this.runtimeDirectory, PENDING_DIRECTORY, `pending-${sessionId}`),
    );
    const attemptId = content?.trim();
    return attemptId !== undefined && ATTEMPT_ID_PATTERN.test(attemptId) ? attemptId : null;
  }

  async readResult(): Promise<PromotionBreadcrumb | null> {
    const content = await readPrivateFile(
      join(this.runtimeDirectory, PENDING_DIRECTORY, RESULT_FILE),
    );
    if (content === null) return null;
    try {
      return parseBreadcrumb(JSON.parse(content) as unknown);
    } catch {
      return null;
    }
  }
}

/** Drives one bounded, non-interactive in-seat promotion attempt. */
export async function promoteSeat(
  seat: string,
  options: PromoteSeatOptions,
): Promise<PromotionResult> {
  const clock = options.clock ?? wallClockSeconds;
  const sleep = options.sleep ?? defaultSleep;
  const timeoutMs = normalizeTimeout(options.timeoutMs);
  const pollIntervalMs = normalizePollInterval(options.pollIntervalMs);
  let target: PromotionTarget;
  try {
    target = options.target ?? (await options.transport.resolve(seat));
  } catch (error: unknown) {
    return unverifiedUnresolvedSeat(seat, `RESOLVE_FAILED: ${errorMessage(error)}`);
  }
  const previousAttemptId = await options.store.readAttemptId(target.sessionId);
  const preSendTimestamp = clock();
  try {
    await options.transport.sendPromotion(target);
  } catch (error: unknown) {
    return unverified(target, `DELIVERY_FAILED: ${errorMessage(error)}`);
  }

  const deadline = preSendTimestamp + timeoutMs / 1_000;
  let attemptId: string | null = null;
  while (true) {
    const currentAttemptId = await options.store.readAttemptId(target.sessionId);
    if (currentAttemptId !== null && currentAttemptId !== previousAttemptId) {
      attemptId = currentAttemptId;
    }
    if (attemptId !== null || previousAttemptId === null) {
      // Completion can consume a first attempt's nonce before this poll observes it.
      // In that branch, correlation degrades to session_id + fresh timestamp, which
      // is acceptable for this 0600, same-UID local trust boundary.
      const breadcrumb = await options.store.readResult();
      if (
        breadcrumb !== null &&
        breadcrumb.session_id === target.sessionId &&
        (attemptId === null || breadcrumb.attempt_id === attemptId) &&
        breadcrumb.ts > preSendTimestamp
      ) {
        return {
          bundle: target.bundle,
          expiresAtWallclock: breadcrumb.expires_at_wallclock,
          reason: breadcrumb.reason,
          seat: target.seat,
          sessionId: target.sessionId,
          status: breadcrumb.verified ? 'VERIFIED' : 'UNVERIFIED',
        };
      }
    }
    if (clock() >= deadline) return unverified(target, 'PROMOTION_TIMEOUT');
    await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - clock()) * 1_000));
  }
}

export function registerPromoteCommand(program: Command, deps: PromoteCommandDeps = {}): void {
  const mosaicHome = deps.mosaicHome ?? resolveFleetPaths().mosaicHome;
  const transport =
    deps.transport ?? new TmuxPromotionTransport({ mosaicHome, runner: deps.runner ?? runCommand });
  const store = deps.store ?? new FilePromotionBreadcrumbStore();

  program
    .command('promote <seat>')
    .description('Promote a Claude fleet seat and report the correlated lease result')
    .option(
      '--timeout <ms>',
      `Bounded result wait in milliseconds (default: ${DEFAULT_TIMEOUT_MS})`,
    )
    .action(async (seat: string, opts: { timeout?: string }) => {
      if (
        process.env['MOSAIC_LEASE_SESSION_ID'] !== undefined &&
        deps.mintAuthorization === undefined
      ) {
        console.error('mosaic promote must run outside a lease-gated seat.');
        process.exitCode = 1;
        return;
      }
      let target: PromotionTarget;
      try {
        target = await transport.resolve(seat);
        await (deps.mintAuthorization ?? mintAuthorization)(target);
      } catch (error: unknown) {
        console.error(`mosaic promote authorization failed: ${errorMessage(error)}`);
        process.exitCode = 1;
        return;
      }
      const result = await promoteSeat(seat, {
        store,
        target,
        timeoutMs: parseOptionTimeout(opts.timeout),
        transport,
      });
      const expiry =
        result.expiresAtWallclock === null
          ? 'none'
          : new Date(result.expiresAtWallclock * 1_000).toISOString();
      const reason = result.reason === null ? '' : ` reason=${result.reason}`;
      console.log(
        `${result.status} seat=${result.seat} session=${result.sessionId} bundle=${result.bundle} expiry=${expiry}${reason}`,
      );
      if (result.status === 'UNVERIFIED') process.exitCode = 1;
    });
}

async function mintAuthorization(target: PromotionTarget): Promise<void> {
  const directory = join(defaultRuntimeDirectory(), PENDING_DIRECTORY, AUTHORIZATION_DIRECTORY);
  await mkdir(directory, { mode: 0o700, recursive: true });
  const token = {
    expires_at: wallClockSeconds() + AUTHORIZATION_TTL_SECONDS,
    nonce: randomBytes(32).toString('hex'),
    seat: target.seat,
    session_id: target.sessionId,
    ts: wallClockSeconds(),
  };
  const destination = join(directory, `${target.sessionId}.auth`);
  const temporary = join(directory, `.${target.sessionId}.${randomBytes(8).toString('hex')}.tmp`);
  const handle = await open(
    temporary,
    constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
    0o600,
  );
  try {
    await handle.chmod(0o600);
    await handle.writeFile(JSON.stringify(token));
    await handle.sync();
  } finally {
    await handle.close();
  }
  await rename(temporary, destination);
}

function defaultRuntimeDirectory(): string {
  const configured = process.env['XDG_RUNTIME_DIR'];
  if (configured) return configured;
  const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
  return `/run/user/${uid}`;
}

async function readPrivateFile(path: string): Promise<string | null> {
  let handle: Awaited<ReturnType<typeof open>>;
  try {
    handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
  } catch {
    return null;
  }
  try {
    const metadata = await handle.stat();
    if (
      !metadata.isFile() ||
      metadata.uid !== (typeof process.getuid === 'function' ? process.getuid() : 0) ||
      (metadata.mode & 0o077) !== 0
    ) {
      return null;
    }
    return handle.readFile({ encoding: 'utf8' });
  } catch {
    return null;
  } finally {
    await handle.close();
  }
}

function parseBreadcrumb(value: unknown): PromotionBreadcrumb | null {
  if (!isRecord(value) || Object.keys(value).length !== 6) return null;
  const { attempt_id, expires_at_wallclock, reason, session_id, ts, verified } = value;
  if (
    typeof attempt_id !== 'string' ||
    !ATTEMPT_ID_PATTERN.test(attempt_id) ||
    typeof verified !== 'boolean' ||
    typeof session_id !== 'string' ||
    !ATTEMPT_ID_PATTERN.test(session_id) ||
    typeof ts !== 'number' ||
    !Number.isFinite(ts) ||
    (expires_at_wallclock !== null &&
      (typeof expires_at_wallclock !== 'number' || !Number.isFinite(expires_at_wallclock))) ||
    (reason !== null && typeof reason !== 'string')
  ) {
    return null;
  }
  return { attempt_id, expires_at_wallclock, reason, session_id, ts, verified };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function normalizeTimeout(value: number | undefined): number {
  return value !== undefined && Number.isFinite(value) ? Math.max(0, value) : DEFAULT_TIMEOUT_MS;
}

function normalizePollInterval(value: number | undefined): number {
  return value !== undefined && Number.isFinite(value)
    ? Math.max(1, value)
    : DEFAULT_POLL_INTERVAL_MS;
}

function parseOptionTimeout(value: string | undefined): number | undefined {
  if (value === undefined) return undefined;
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function unverifiedUnresolvedSeat(seat: string, reason: string): PromotionResult {
  return {
    bundle: 'unresolved',
    expiresAtWallclock: null,
    reason,
    seat,
    sessionId: 'unresolved',
    status: 'UNVERIFIED',
  };
}

function unverified(target: PromotionTarget, reason: string): PromotionResult {
  return {
    bundle: target.bundle,
    expiresAtWallclock: null,
    reason,
    seat: target.seat,
    sessionId: target.sessionId,
    status: 'UNVERIFIED',
  };
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function wallClockSeconds(): number {
  return Date.now() / 1_000;
}

function defaultSleep(milliseconds: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

function runCommand(
  command: string,
  args: string[],
): Promise<{
  exitCode: number;
  stderr: string;
  stdout: string;
}> {
  return new Promise((resolve) => {
    const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
    let stdout = '';
    let stderr = '';
    let settled = false;
    const finish = (result: { exitCode: number; stderr: string; stdout: string }): void => {
      if (settled) return;
      settled = true;
      clearTimeout(timeout);
      resolve(result);
    };
    const timeout = setTimeout(() => {
      child.kill('SIGKILL');
      finish({
        exitCode: 124,
        stderr: `Promotion transport subprocess timed out after ${SUBPROCESS_TIMEOUT_MS}ms.`,
        stdout,
      });
    }, SUBPROCESS_TIMEOUT_MS);
    child.stdout.on('data', (chunk: Buffer) => {
      stdout += chunk.toString('utf8');
    });
    child.stderr.on('data', (chunk: Buffer) => {
      stderr += chunk.toString('utf8');
    });
    child.on('error', (error: Error) => {
      finish({ exitCode: 127, stderr: error.message, stdout });
    });
    child.on('close', (code: number | null) => {
      finish({ exitCode: code ?? 1, stderr, stdout });
    });
  });
}
