import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { Command } from 'commander';
import {
  parseV1MigrationObservations,
  parseV1ToV2MigrationDecisions,
  previewV1ToV2Migration,
} from '../fleet/v1-v2-migration.js';

export interface FleetMigrationCommandDeps {
  readonly mosaicHome?: string;
  readonly rolesDir?: string;
  readonly overrideDir?: string;
  readonly readText?: (path: string) => Promise<string>;
  readonly printJson?: (value: unknown) => void;
  readonly setExitCode?: (code: number) => void;
}

interface PreviewOptions {
  readonly source?: string | boolean;
  readonly decisions?: string | boolean;
  readonly observations?: string | boolean;
}

/** Registers preview-only v1 migration. This command has no mutation verbs or runners. */
export function registerFleetMigrationCommand(
  fleetCommand: Command,
  deps: FleetMigrationCommandDeps = {},
): void {
  fleetCommand
    .command('migrate-v1')
    .description('Preview a field-complete v1-to-v2 roster migration')
    .command('preview')
    .description('Compile migration evidence without writing files or changing runtimes')
    .option('--source [path]', 'v1 roster YAML or JSON')
    .option('--decisions [path]', 'explicit migration decisions JSON')
    .option('--observations [path]', 'reviewed lifecycle observations JSON')
    .action(async (options: PreviewOptions): Promise<void> => {
      const readText = deps.readText ?? ((path: string): Promise<string> => readFile(path, 'utf8'));
      const printJson =
        deps.printJson ?? ((value: unknown): void => console.log(JSON.stringify(value)));
      const setExitCode =
        deps.setExitCode ?? ((code: number): void => void (process.exitCode = code));
      try {
        const requiredOptionNames = ['source', 'decisions', 'observations'] as const;
        const missingOption = requiredOptionNames.find((name) => options[name] === undefined);
        if (missingOption !== undefined) {
          printJson({
            status: 'blocked',
            blockers: [
              {
                code: 'missing-migration-preview-option',
                path: `request.${missingOption}`,
                detail: 'Required migration preview option is missing.',
              },
            ],
          });
          setExitCode(1);
          return;
        }
        const missingValueOption = requiredOptionNames.find((name) => options[name] === true);
        if (missingValueOption !== undefined) {
          printJson({
            status: 'blocked',
            blockers: [
              {
                code: 'missing-migration-preview-option-value',
                path: `request.${missingValueOption}`,
                detail: 'Required migration preview option value is missing.',
              },
            ],
          });
          setExitCode(1);
          return;
        }
        const emptyOption = requiredOptionNames.find(
          (name) => typeof options[name] === 'string' && options[name].trim() === '',
        );
        if (emptyOption !== undefined) {
          printJson({
            status: 'blocked',
            blockers: [
              {
                code: 'empty-migration-preview-option',
                path: `request.${emptyOption}`,
                detail: 'Required migration preview option must be a non-empty path.',
              },
            ],
          });
          setExitCode(1);
          return;
        }
        const sourcePath = options.source;
        const decisionsPath = options.decisions;
        const observationsPath = options.observations;
        if (
          typeof sourcePath !== 'string' ||
          typeof decisionsPath !== 'string' ||
          typeof observationsPath !== 'string'
        ) {
          throw new Error('Validated migration preview options became unavailable.');
        }
        const [source, decisionsSource, observationsSource] = await Promise.all([
          readText(sourcePath),
          readText(decisionsPath),
          readText(observationsPath),
        ]);
        const decisions = parseV1ToV2MigrationDecisions(
          parseJsonObject(decisionsSource, 'migration decisions'),
        );
        const observations = parseV1MigrationObservations(
          parseJsonObject(observationsSource, 'lifecycle observations'),
        );
        const mosaicHome =
          deps.mosaicHome ?? fleetCommand.opts<{ mosaicHome: string }>().mosaicHome;
        const preview = await previewV1ToV2Migration({
          source,
          sourcePath,
          decisions,
          observations,
          personaDirs: {
            rolesDir: deps.rolesDir ?? join(mosaicHome, 'fleet', 'roles'),
            overrideDir: deps.overrideDir ?? join(mosaicHome, 'fleet', 'roles.local'),
          },
          environment: {
            mosaicHome,
            agentEnvDir: join(mosaicHome, 'fleet', 'agents'),
          },
        });
        printJson(preview);
        if (preview.status === 'blocked') setExitCode(1);
      } catch (error: unknown) {
        printJson({
          status: 'blocked',
          blockers: [
            {
              code: 'migration-preview-failed',
              path: 'request',
              detail: safeErrorDetail(error),
            },
          ],
        });
        setExitCode(1);
      }
    });
}

function parseJsonObject(source: string, label: string): unknown {
  let value: unknown;
  try {
    value = JSON.parse(source) as unknown;
  } catch {
    throw new Error(`${label} must be valid JSON.`);
  }
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    throw new Error(`${label} must be a JSON object.`);
  }
  return value;
}

function safeErrorDetail(error: unknown): string {
  if (error instanceof Error && isPublishableValidationMessage(error.message)) return error.message;
  return 'Migration preview failed without publishable detail.';
}

function isPublishableValidationMessage(message: string): boolean {
  return /^(migration decisions|lifecycle observations) must be (valid JSON|a JSON object)\.$/.test(
    message,
  );
}
