import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { Command } from 'commander';
import {
  executeFleetAgentMutation,
  FleetAgentMutationError,
  type FleetAgentMutationAgent,
  type FleetAgentMutationOperation,
  type FleetAgentMutationOptions,
  type FleetAgentMutationRequest,
  type FleetAgentMutationResult,
} from '../fleet/fleet-agent-crud.js';
import {
  parseRosterV2,
  ROSTER_V2_REASONING_LEVELS,
  ROSTER_V2_SUPPORTED_RUNTIMES,
  type FleetRosterV2,
  type RosterV2ReasoningLevel,
  type RosterV2RuntimeName,
} from '../fleet/roster-v2.js';

export interface FleetAgentCrudCommandDeps {
  readonly mosaicHome?: string;
  /** Test seam for deterministic post-roster filesystem projection failures. */
  readonly projectionApplier?: FleetAgentMutationOptions['projectionApplier'];
}

interface MutationOptions {
  readonly expectedGeneration: string;
  readonly agent?: string;
  readonly dryRun?: boolean;
  readonly persistedStart?: boolean;
}

const AGENT_REQUEST_KEYS = new Set([
  'name',
  'alias',
  'className',
  'runtime',
  'provider',
  'model',
  'reasoning',
  'toolPolicy',
  'workingDirectory',
  'persistentPersona',
  'resetBetweenTasks',
  'launch',
]);
const AGENT_LAUNCH_REQUEST_KEYS = new Set(['yolo']);

/** Registers only roster-v2 desired-state mutations; it never invokes runtime actions. */
export function registerFleetAgentCrudCommands(
  agentCommand: Command,
  deps: FleetAgentCrudCommandDeps = {},
): void {
  agentCommand
    .command('get <name>')
    .description('Read one local roster-v2 agent as JSON')
    .action(async (name: string): Promise<void> => {
      await writeJsonOutcome(async (): Promise<void> => {
        const roster = await loadRoster(agentCommand, deps);
        const agent = roster.agents.find((candidate): boolean => candidate.name === name);
        if (!agent)
          throw new FleetAgentMutationError('agent-not-found', `Agent "${name}" is not in roster.`);
        printJson({ generation: roster.generation, agent });
      });
    });

  registerMutationCommand(agentCommand, deps, 'create');
  registerMutationCommand(agentCommand, deps, 'update');
  registerMutationCommand(agentCommand, deps, 'delete');

  agentCommand
    .command('plan <operation> [name]')
    .description('Plan a local roster-v2 mutation without writing roster or projections')
    .requiredOption('--expected-generation <number>', 'Authoritative roster generation')
    .option('--agent <json>', 'JSON agent payload for create or update')
    .option(
      '--persisted-start',
      'Plan create with desired_state: running without starting a runtime',
    )
    .action(
      async (operation: string, name: string | undefined, opts: MutationOptions): Promise<void> => {
        await writeJsonOutcome(async (): Promise<void> => {
          const parsedOperation = parseOperation(operation);
          await executeCommand(
            agentCommand,
            deps,
            parsedOperation,
            opts,
            true,
            parsePlanTargetName(parsedOperation, name),
          );
        });
      },
    );
}

function registerMutationCommand(
  agentCommand: Command,
  deps: FleetAgentCrudCommandDeps,
  operation: FleetAgentMutationOperation,
): void {
  const command = agentCommand
    .command(
      operation === 'update'
        ? 'update <name>'
        : `${operation}${operation === 'delete' ? ' <name>' : ''}`,
    )
    .description(`${operation} a local roster-v2 agent without runtime actions`)
    .requiredOption('--expected-generation <number>', 'Authoritative roster generation')
    .option('--agent <json>', 'JSON agent payload for create or update')
    .option('--dry-run', 'Validate and plan without writing roster or projections');

  if (operation === 'create') {
    command.option(
      '--persisted-start',
      'Persist desired_state: running without starting a runtime',
    );
    command.action(async (opts: MutationOptions): Promise<void> => {
      await writeJsonOutcome(async (): Promise<void> => {
        await executeCommand(agentCommand, deps, operation, opts, false);
      });
    });
    return;
  }

  command.action(async (name: string, opts: MutationOptions): Promise<void> => {
    await writeJsonOutcome(async (): Promise<void> => {
      await executeCommand(agentCommand, deps, operation, opts, false, name);
    });
  });
}

async function executeCommand(
  agentCommand: Command,
  deps: FleetAgentCrudCommandDeps,
  operation: FleetAgentMutationOperation,
  opts: MutationOptions,
  forceDryRun: boolean,
  name?: string,
): Promise<void> {
  const mosaicHome = resolveMosaicHome(agentCommand, deps);
  const rosterPath = resolveRosterPath(agentCommand, mosaicHome);
  const roster = parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml');
  const request = buildRequest(operation, opts, name);
  const result = await executeFleetAgentMutation({
    roster,
    request,
    mosaicHome,
    rosterPath,
    agentEnvDir: join(mosaicHome, 'fleet', 'agents'),
    rolesDir: join(mosaicHome, 'fleet', 'roles'),
    overrideDir: join(mosaicHome, 'fleet', 'roles.local'),
    dryRun: forceDryRun || opts.dryRun === true,
    ...(deps.projectionApplier === undefined ? {} : { projectionApplier: deps.projectionApplier }),
  });
  printJson(result);
  process.exitCode = fleetAgentMutationExitCode(result);
}

/** A persisted roster with failed derived projections is a non-zero CLI outcome. */
export function fleetAgentMutationExitCode(
  result: Pick<FleetAgentMutationResult, 'recovery'>,
): 0 | 1 {
  return result.recovery === undefined ? 0 : 1;
}

function parsePlanTargetName(
  operation: FleetAgentMutationOperation,
  name: string | undefined,
): string | undefined {
  if (operation === 'create') {
    if (name !== undefined) {
      throw new FleetAgentMutationError(
        'invalid-request',
        'Create plan does not accept a target name.',
      );
    }
    return undefined;
  }
  if (name === undefined) {
    throw new FleetAgentMutationError(
      'invalid-request',
      `${operation} plan requires a target agent name.`,
    );
  }
  return name;
}

function buildRequest(
  operation: FleetAgentMutationOperation,
  opts: MutationOptions,
  name?: string,
): FleetAgentMutationRequest {
  const expectedGeneration = parseExpectedGeneration(opts.expectedGeneration);
  const agent = opts.agent === undefined ? undefined : parseAgent(opts.agent);
  if ((operation === 'create' || operation === 'update') && !agent) {
    throw new FleetAgentMutationError('invalid-request', `${operation} requires --agent JSON.`);
  }
  return {
    operation,
    expectedGeneration,
    ...(name === undefined ? {} : { name }),
    ...(agent === undefined ? {} : { agent }),
    ...(operation === 'create' && opts.persistedStart === true ? { persistedStart: true } : {}),
  };
}

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

function parseOperation(value: string): FleetAgentMutationOperation {
  if (value === 'create' || value === 'update' || value === 'delete') return value;
  throw new FleetAgentMutationError(
    'invalid-request',
    'plan operation must be create, update, or delete.',
  );
}

function parseAgent(source: string): FleetAgentMutationAgent {
  let value: unknown;
  try {
    value = JSON.parse(source);
  } catch {
    throw new FleetAgentMutationError('invalid-request', '--agent must be valid JSON.');
  }
  if (!isRecord(value)) {
    throw new FleetAgentMutationError('invalid-request', '--agent must be a JSON object.');
  }
  rejectUnknownKeys(value, AGENT_REQUEST_KEYS, '--agent');

  const runtime = requiredRuntime(value, 'runtime');
  const reasoning = requiredReasoning(value, 'reasoning');
  const launch = requiredRecord(value, 'launch');
  rejectUnknownKeys(launch, AGENT_LAUNCH_REQUEST_KEYS, '--agent.launch');
  return {
    name: requiredString(value, 'name'),
    alias: requiredString(value, 'alias'),
    className: requiredString(value, 'className'),
    runtime,
    provider: requiredString(value, 'provider'),
    model: requiredString(value, 'model'),
    reasoning,
    toolPolicy: requiredString(value, 'toolPolicy'),
    workingDirectory: requiredString(value, 'workingDirectory'),
    persistentPersona: requiredBoolean(value, 'persistentPersona'),
    resetBetweenTasks: requiredBoolean(value, 'resetBetweenTasks'),
    launch: { yolo: requiredBoolean(launch, 'yolo') },
  };
}

async function loadRoster(
  agentCommand: Command,
  deps: FleetAgentCrudCommandDeps,
): Promise<FleetRosterV2> {
  const mosaicHome = resolveMosaicHome(agentCommand, deps);
  const rosterPath = resolveRosterPath(agentCommand, mosaicHome);
  return parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml');
}

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

function resolveRosterPath(agentCommand: Command, mosaicHome: string): string {
  const opts = agentCommand.optsWithGlobals<{ roster?: string }>();
  const canonical = join(mosaicHome, 'fleet', 'roster.yaml');
  if (opts.roster !== undefined && resolve(opts.roster) !== resolve(canonical)) {
    throw new FleetAgentMutationError(
      'invalid-request',
      'Roster-v2 mutations require the canonical <mosaic-home>/fleet/roster.yaml path.',
    );
  }
  return canonical;
}

function rejectUnknownKeys(
  value: Record<string, unknown>,
  allowedKeys: ReadonlySet<string>,
  field: string,
): void {
  const hasUnsupportedOwnProperty = Object.getOwnPropertyNames(value).some(
    (key: string): boolean => !allowedKeys.has(key),
  );
  if (hasUnsupportedOwnProperty || Object.getOwnPropertySymbols(value).length > 0) {
    throw new FleetAgentMutationError('invalid-request', `${field} contains unsupported keys.`);
  }
}

function requiredString(value: Record<string, unknown>, key: string): string {
  const candidate = requiredOwnValue(value, key);
  if (typeof candidate !== 'string' || candidate.length === 0) {
    throw new FleetAgentMutationError(
      'invalid-request',
      `--agent.${key} must be a non-empty string.`,
    );
  }
  return candidate;
}

function requiredBoolean(value: Record<string, unknown>, key: string): boolean {
  const candidate = requiredOwnValue(value, key);
  if (typeof candidate !== 'boolean') {
    throw new FleetAgentMutationError('invalid-request', `--agent.${key} must be a boolean.`);
  }
  return candidate;
}

function requiredRecord(value: Record<string, unknown>, key: string): Record<string, unknown> {
  const candidate = requiredOwnValue(value, key);
  if (!isRecord(candidate)) {
    throw new FleetAgentMutationError('invalid-request', `--agent.${key} must be an object.`);
  }
  return candidate;
}

function requiredRuntime(value: Record<string, unknown>, key: string): RosterV2RuntimeName {
  const candidate = requiredString(value, key);
  if (!isRuntime(candidate)) {
    throw new FleetAgentMutationError(
      'invalid-request',
      `--agent.${key} is not a supported runtime.`,
    );
  }
  return candidate;
}

function requiredReasoning(value: Record<string, unknown>, key: string): RosterV2ReasoningLevel {
  const candidate = requiredString(value, key);
  if (!isReasoning(candidate)) {
    throw new FleetAgentMutationError(
      'invalid-request',
      `--agent.${key} is not a supported reasoning level.`,
    );
  }
  return candidate;
}

function isRuntime(value: string): value is RosterV2RuntimeName {
  return ROSTER_V2_SUPPORTED_RUNTIMES.some(
    (runtime: RosterV2RuntimeName): boolean => runtime === value,
  );
}

function isReasoning(value: string): value is RosterV2ReasoningLevel {
  return ROSTER_V2_REASONING_LEVELS.some(
    (reasoning: RosterV2ReasoningLevel): boolean => reasoning === value,
  );
}

function requiredOwnValue(value: Record<string, unknown>, key: string): unknown {
  if (!Object.hasOwn(value, key)) {
    throw new FleetAgentMutationError('invalid-request', `--agent.${key} is required.`);
  }
  return value[key];
}

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

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

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