import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { Command } from 'commander';
import type { CommandRunner } from './fleet.js';
import {
  executeFleetReconcile,
  FleetReconcileError,
  type FleetReconcileCommand,
  type FleetReconcileDeps,
} from '../fleet/fleet-reconciler.js';
import { parseRosterV2 } from '../fleet/roster-v2.js';

export interface FleetReconcilerCommandDeps {
  readonly runner: CommandRunner;
  readonly mosaicHome?: string;
  readonly reconcileDeps?: Omit<FleetReconcileDeps, 'runner' | 'mosaicHome'>;
}

interface ReconcileOptions {
  readonly expectedGeneration?: string;
  readonly dryRun?: boolean;
}

/** Registers roster-v2 reconciliation commands on the canonical fleet control plane. */
export function registerFleetReconcilerCommands(
  fleetCommand: Command,
  deps: FleetReconcilerCommandDeps,
): void {
  for (const operation of ['apply', 'reconcile'] as const) {
    fleetCommand
      .command(operation)
      .description(`${operation} local roster-owned projections and desired lifecycle`)
      .requiredOption('--expected-generation <number>', 'Authoritative roster generation')
      .option('--dry-run', 'Plan and preflight without writing projections or lifecycle state')
      .action(async (opts: ReconcileOptions): Promise<void> => {
        await writeOutcome(async (): Promise<void> => {
          await executeReconcilerCommand(fleetCommand, deps, operation, opts);
        });
      });
  }

  fleetCommand
    .command('doctor')
    .description('Classify local roster-owned drift without mutation')
    .action(async (): Promise<void> => {
      await writeOutcome(async (): Promise<void> => {
        await executeReconcilerCommand(fleetCommand, deps, 'doctor', {});
      });
    });
}

export async function executeReconcilerCommandJson(
  fleetCommand: Command,
  deps: FleetReconcilerCommandDeps,
  operation: FleetReconcileCommand,
  opts: ReconcileOptions,
  agentName?: string,
): Promise<void> {
  await writeOutcome(async (): Promise<void> => {
    await executeReconcilerCommand(fleetCommand, deps, operation, opts, agentName);
  });
}

export async function executeReconcilerCommand(
  fleetCommand: Command,
  deps: FleetReconcilerCommandDeps,
  operation: FleetReconcileCommand,
  opts: ReconcileOptions,
  agentName?: string,
): Promise<void> {
  const mosaicHome = resolveMosaicHome(fleetCommand, deps);
  const rosterPath = resolveRosterPath(fleetCommand, mosaicHome);
  const roster = parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml');
  const mutating = operation === 'apply' || operation === 'reconcile' || isLifecycle(operation);
  const expectedGeneration = mutating
    ? parseExpectedGeneration(opts.expectedGeneration)
    : undefined;
  const result = await executeFleetReconcile({
    roster,
    command: opts.dryRun === true ? 'plan' : operation,
    ...(agentName === undefined ? {} : { agentName }),
    ...(expectedGeneration === undefined ? {} : { expectedGeneration }),
    deps: {
      runner: async (command: string, args: readonly string[]) => deps.runner(command, [...args]),
      mosaicHome,
      rolesDir: join(mosaicHome, 'fleet', 'roles'),
      overrideDir: join(mosaicHome, 'fleet', 'roles.local'),
      readRoster: async (): Promise<typeof roster> =>
        parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml'),
      ...(deps.reconcileDeps ?? {}),
    },
  });
  printJson(result);
  process.exitCode = result.recovery === undefined && result.cleanup === undefined ? 0 : 1;
}

function isLifecycle(operation: FleetReconcileCommand): boolean {
  return operation === 'start' || operation === 'stop' || operation === 'restart';
}

function parseExpectedGeneration(value: string | undefined): number {
  const generation = Number(value);
  if (!Number.isSafeInteger(generation) || generation < 1) {
    throw new FleetReconcileError(
      'lifecycle-precondition-failed',
      '--expected-generation must be a positive safe integer.',
    );
  }
  return generation;
}

function resolveMosaicHome(fleetCommand: Command, deps: FleetReconcilerCommandDeps): string {
  const options = fleetCommand.optsWithGlobals<{ mosaicHome?: string }>();
  return (
    options.mosaicHome ?? deps.mosaicHome ?? join(process.env['HOME'] ?? '', '.config', 'mosaic')
  );
}

function resolveRosterPath(fleetCommand: Command, mosaicHome: string): string {
  const options = fleetCommand.optsWithGlobals<{ roster?: string }>();
  const canonical = join(mosaicHome, 'fleet', 'roster.yaml');
  if (options.roster !== undefined && resolve(options.roster) !== resolve(canonical)) {
    throw new FleetReconcileError(
      'lifecycle-precondition-failed',
      'Roster-v2 reconciliation requires the canonical roster path.',
    );
  }
  return canonical;
}

async function writeOutcome(action: () => Promise<void>): Promise<void> {
  try {
    await action();
  } catch (error: unknown) {
    process.exitCode = 1;
    printJson({
      error: {
        code: error instanceof FleetReconcileError ? error.code : 'reconcile-failed',
      },
    });
  }
}

function printJson(value: object): void {
  console.log(JSON.stringify(value));
}
