import {
  lstatSync,
  mkdirSync,
  readFileSync,
  readlinkSync,
  readdirSync,
  realpathSync,
  rmSync,
  symlinkSync,
  writeFileSync,
  type Stats,
} from 'node:fs';
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import type { Command } from 'commander';
import {
  harnessHome,
  launchFleetRuntime,
  type FleetHarnessContext,
  type RuntimeName,
} from './launch.js';
import { defaultFleetDataHome } from '../fleet/fleet-agent-scaffold.js';

export const FLEET_AGENT_PROFILE_SCHEMA = 1;
const PROFILE_KEYS = [
  'schema',
  'harness',
  'bundle',
  'model',
  'overlay',
  'plugins',
  'skills',
  'env',
];
const RUNTIMES: readonly RuntimeName[] = ['claude', 'codex', 'opencode', 'pi'];
const AGENT_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const STORE_ENTRY = /^[A-Za-z0-9][A-Za-z0-9_.@-]*$/;
const BUNDLE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.@-]*$/;
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;

const CREDENTIAL_FILES: Record<RuntimeName, string> = {
  claude: '.credentials.json',
  pi: 'auth.json',
  codex: 'auth.json',
  opencode: 'auth.json',
};

export type FleetLaunchErrorCode =
  | 'SCHEMA_TOO_NEW'
  | 'PROFILE_INVALID'
  | 'AGENT_NOT_SCAFFOLDED'
  | 'COMPOSITION_FAILED'
  | 'FIRST_AUTH_REFUSAL';

export class FleetLaunchError extends Error {
  constructor(
    readonly code: FleetLaunchErrorCode,
    message: string,
  ) {
    super(message);
    this.name = 'FleetLaunchError';
  }
}

export interface FleetAgentLaunchProfile {
  readonly schema: 1;
  readonly harness: RuntimeName;
  readonly bundle: string;
  readonly model?: string;
  readonly overlay?: string;
  readonly plugins: readonly string[];
  readonly skills: readonly string[];
  readonly env: Readonly<Record<string, string>>;
}

export interface FleetLaunchRoots {
  readonly systemHome: string;
  readonly userHome: string;
}

interface SettingsLayer {
  readonly name: 'system' | 'user' | 'agent';
  readonly path: string;
  readonly present: boolean;
  readonly value: Record<string, unknown>;
}

interface PlannedLink {
  readonly kind: 'plugin' | 'skill';
  readonly name: string;
  readonly link: string;
  readonly target: string;
}

export interface FleetLaunchComposition {
  readonly name: string;
  readonly profilePath: string;
  readonly profile: FleetAgentLaunchProfile;
  readonly agentDir: string;
  readonly seatHome: string;
  readonly settings: {
    readonly layers: readonly SettingsLayer[];
    readonly merged: Record<string, unknown>;
    readonly output: string;
    readonly snapshot: string;
  };
  readonly bundle: {
    readonly requested: string;
    readonly resolved: string;
    readonly email?: string;
    readonly display: string;
  };
  readonly credential: {
    readonly link: string;
    readonly target: string;
  };
  readonly installs: readonly PlannedLink[];
  readonly prune: readonly string[];
  readonly env: Readonly<Record<string, string>>;
  readonly argv: readonly string[];
}

export interface FleetLaunchCommandDeps {
  readonly userHome?: string;
  readonly launcher?: (
    runtime: RuntimeName,
    args: string[],
    declaredEnv: Readonly<Record<string, string>>,
    context: FleetHarnessContext,
  ) => void;
}

function requiredObject(value: unknown, label: string): Record<string, unknown> {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    throw new FleetLaunchError('PROFILE_INVALID', `${label} must be a JSON object.`);
  }
  return value as Record<string, unknown>;
}

function optionalString(value: unknown, label: string): string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'string' || value.trim() === '') {
    throw new FleetLaunchError('PROFILE_INVALID', `${label} must be a non-empty string.`);
  }
  return value.trim();
}

function stringList(value: unknown, label: string): string[] {
  if (value === undefined) return [];
  if (!Array.isArray(value)) {
    throw new FleetLaunchError(
      'PROFILE_INVALID',
      `${label} must be an array of store entry names.`,
    );
  }
  return value.map((entry: unknown, index: number): string => {
    if (typeof entry !== 'string' || !STORE_ENTRY.test(entry)) {
      throw new FleetLaunchError(
        'PROFILE_INVALID',
        `${label}[${index}] must be a safe store entry name.`,
      );
    }
    return entry;
  });
}

/** Parse and strictly validate the frozen, user-facing per-agent profile schema. */
export function parseFleetAgentProfile(source: string): FleetAgentLaunchProfile {
  let parsed: unknown;
  try {
    parsed = JSON.parse(source) as unknown;
  } catch (error: unknown) {
    const detail = error instanceof Error ? error.message : String(error);
    throw new FleetLaunchError('PROFILE_INVALID', `profile.json is not valid JSON: ${detail}`);
  }
  const raw = requiredObject(parsed, 'profile.json');
  if (!Number.isSafeInteger(raw['schema'])) {
    throw new FleetLaunchError('PROFILE_INVALID', 'profile.json schema is required and must be 1.');
  }
  if ((raw['schema'] as number) > FLEET_AGENT_PROFILE_SCHEMA) {
    throw new FleetLaunchError(
      'SCHEMA_TOO_NEW',
      `SCHEMA_TOO_NEW: profile schema ${String(raw['schema'])} is newer than supported schema ${FLEET_AGENT_PROFILE_SCHEMA}; upgrade Mosaic before launching this agent.`,
    );
  }
  if (raw['schema'] !== FLEET_AGENT_PROFILE_SCHEMA) {
    throw new FleetLaunchError(
      'PROFILE_INVALID',
      `profile.json schema ${String(raw['schema'])} is unsupported; expected schema 1.`,
    );
  }
  const unknown = Object.keys(raw).filter((key: string): boolean => !PROFILE_KEYS.includes(key));
  if (unknown.length > 0) {
    throw new FleetLaunchError(
      'PROFILE_INVALID',
      `unknown profile key "${unknown[0]}" (schema ${String(raw['schema'])})`,
    );
  }
  if (typeof raw['harness'] !== 'string' || !RUNTIMES.includes(raw['harness'] as RuntimeName)) {
    throw new FleetLaunchError(
      'PROFILE_INVALID',
      `profile.json harness is required and must be one of: ${RUNTIMES.join(', ')}.`,
    );
  }
  const bundle = optionalString(raw['bundle'], 'profile.json bundle') ?? 'primary';
  if (!BUNDLE_NAME.test(bundle)) {
    throw new FleetLaunchError(
      'PROFILE_INVALID',
      'profile.json bundle must be a safe bundle name.',
    );
  }
  const overlay = optionalString(raw['overlay'], 'profile.json overlay');
  if (overlay !== undefined && (isAbsolute(overlay) || overlay.split(/[\\/]/u).includes('..'))) {
    throw new FleetLaunchError(
      'PROFILE_INVALID',
      'profile.json overlay must remain inside the agent directory.',
    );
  }
  const rawEnv = raw['env'] === undefined ? {} : requiredObject(raw['env'], 'profile.json env');
  const env: Record<string, string> = {};
  for (const [key, value] of Object.entries(rawEnv)) {
    if (!ENV_NAME.test(key) || typeof value !== 'string') {
      throw new FleetLaunchError(
        'PROFILE_INVALID',
        `profile.json env entry "${key}" must have a valid name and string value.`,
      );
    }
    env[key] = value;
  }
  const model = optionalString(raw['model'], 'profile.json model');
  return {
    schema: 1,
    harness: raw['harness'] as RuntimeName,
    bundle,
    ...(model === undefined ? {} : { model }),
    ...(overlay === undefined ? {} : { overlay }),
    plugins: stringList(raw['plugins'], 'profile.json plugins'),
    skills: stringList(raw['skills'], 'profile.json skills'),
    env,
  };
}

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

function cloneValue(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(cloneValue);
  if (isPlainObject(value)) {
    return Object.fromEntries(
      Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]),
    );
  }
  return value;
}

type MergeContext = 'root' | 'hooks' | 'nested';

function mergeObject(
  lower: Record<string, unknown>,
  higher: Record<string, unknown>,
  context: MergeContext = 'nested',
): Record<string, unknown> {
  const result = cloneValue(lower) as Record<string, unknown>;
  for (const [key, highValue] of Object.entries(higher)) {
    if (highValue === null) {
      delete result[key];
      continue;
    }
    const lowValue = result[key];
    if (context === 'hooks' && Array.isArray(lowValue) && Array.isArray(highValue)) {
      result[key] = [...(lowValue as unknown[]), ...(cloneValue(highValue) as unknown[])];
      continue;
    }
    result[key] =
      isPlainObject(lowValue) && isPlainObject(highValue)
        ? mergeObject(
            lowValue,
            highValue,
            context === 'root' && key === 'hooks' ? 'hooks' : 'nested',
          )
        : cloneValue(highValue);
  }
  return result;
}

/**
 * Deep object merge. Scalars and arrays replace; null in a higher layer
 * deletes. Exception: hook event arrays directly under the top-level `hooks`
 * key concatenate (base entries first), so an overlay ADDS gating without
 * erasing the base QA hooks that share an event — replacing them would make
 * the gap-7 base/overlay split unimplementable without duplicating base
 * hooks inside the lease overlay. Removing an event entirely still works via
 * the null tombstone.
 */
export function deepMergeSettings(
  ...layers: ReadonlyArray<Record<string, unknown> | undefined>
): Record<string, unknown> {
  return layers.reduce<Record<string, unknown>>(
    (merged, layer) => (layer === undefined ? merged : mergeObject(merged, layer, 'root')),
    {},
  );
}

function lstatIfPresent(path: string): Stats | undefined {
  try {
    return lstatSync(path);
  } catch (error: unknown) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
    throw error;
  }
}

function assertRealDirectory(path: string, label: string): void {
  const info = lstatIfPresent(path);
  if (!info?.isDirectory() || info.isSymbolicLink()) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `${label} must be a real, non-symlink directory: ${path}`,
    );
  }
}

function assertContained(root: string, candidate: string, label: string): void {
  const rel = relative(resolve(root), resolve(candidate));
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `${label} resolves outside ${root}: ${candidate}`,
    );
  }
}

function readSettingsLayer(
  name: SettingsLayer['name'],
  path: string,
  required: boolean,
): SettingsLayer {
  const info = lstatIfPresent(path);
  if (!info) {
    if (required) {
      throw new FleetLaunchError(
        'COMPOSITION_FAILED',
        `required ${name} settings missing: ${path}`,
      );
    }
    return { name, path, present: false, value: {} };
  }
  if (!info.isFile() || info.isSymbolicLink()) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `${name} settings must be a real, non-symlink JSON file: ${path}`,
    );
  }
  let value: unknown;
  try {
    value = JSON.parse(readFileSync(path, 'utf8')) as unknown;
  } catch (error: unknown) {
    const detail = error instanceof Error ? error.message : String(error);
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `${name} settings are invalid JSON: ${detail}`,
    );
  }
  if (!isPlainObject(value)) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `${name} settings must contain a JSON object.`,
    );
  }
  return { name, path, present: true, value };
}

function accountEmail(bundleDir: string): string | undefined {
  const path = join(bundleDir, 'account.json');
  const info = lstatIfPresent(path);
  if (!info?.isFile() || info.isSymbolicLink()) return undefined;
  try {
    const account = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
    const oauth = isPlainObject(account['oauthAccount']) ? account['oauthAccount'] : undefined;
    for (const value of [oauth?.['emailAddress'], account['emailAddress'], account['email']]) {
      if (typeof value === 'string' && value.trim() !== '') return value.trim();
    }
  } catch {
    return undefined;
  }
  return undefined;
}

function resolveCredential(
  profile: FleetAgentLaunchProfile,
  userHome: string,
  seatHome: string,
): Pick<FleetLaunchComposition, 'bundle' | 'credential'> {
  const authRoot = join(userHome, 'auth', profile.harness);
  assertRealDirectory(authRoot, `${profile.harness} auth root`);
  const bundlePath = join(authRoot, profile.bundle);
  const bundleInfo = lstatIfPresent(bundlePath);
  if (!bundleInfo) {
    throw new FleetLaunchError('COMPOSITION_FAILED', `credential bundle not found: ${bundlePath}`);
  }
  if (bundleInfo.isSymbolicLink() && profile.bundle !== 'primary') {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `only the primary bundle may be an alias symlink: ${bundlePath}`,
    );
  }
  let resolvedBundleDir: string;
  try {
    resolvedBundleDir = realpathSync(bundlePath);
  } catch (error: unknown) {
    const detail = error instanceof Error ? error.message : String(error);
    throw new FleetLaunchError('COMPOSITION_FAILED', `credential bundle cannot resolve: ${detail}`);
  }
  assertContained(realpathSync(authRoot), resolvedBundleDir, 'credential bundle');
  assertRealDirectory(resolvedBundleDir, 'resolved credential bundle');

  const credentialTarget = join(resolvedBundleDir, CREDENTIAL_FILES[profile.harness]);
  const credentialInfo = lstatIfPresent(credentialTarget);
  if (!credentialInfo?.isFile() || credentialInfo.isSymbolicLink()) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `bundle credential must be a real, non-symlink credential file: ${credentialTarget}`,
    );
  }
  const resolvedCredential = realpathSync(credentialTarget);
  assertContained(realpathSync(authRoot), resolvedCredential, 'bundle credential');

  const credentialLink = join(seatHome, CREDENTIAL_FILES[profile.harness]);
  const seatInfo = lstatIfPresent(credentialLink);
  if (seatInfo && !seatInfo.isSymbolicLink()) {
    throw new FleetLaunchError(
      'FIRST_AUTH_REFUSAL',
      `first-auth state detected at ${credentialLink}; refusing to delete or overwrite the real credential file. Enroll or promote it explicitly.`,
    );
  }

  const resolvedName = basename(resolvedBundleDir);
  const email = accountEmail(resolvedBundleDir);
  const display =
    profile.bundle === resolvedName
      ? `${resolvedName}${email ? ` (${email})` : ''}`
      : `${profile.bundle} -> ${resolvedName}${email ? ` (${email})` : ''}`;
  return {
    bundle: {
      requested: profile.bundle,
      resolved: resolvedName,
      ...(email === undefined ? {} : { email }),
      display,
    },
    credential: { link: credentialLink, target: resolvedCredential },
  };
}

function resolveManagedLinks(
  kind: PlannedLink['kind'],
  names: readonly string[],
  userHome: string,
  seatHome: string,
): { installs: PlannedLink[]; prune: string[] } {
  const plural = kind === 'plugin' ? 'plugins' : 'skills';
  const storeRoot = join(userHome, plural);
  const installRoot = join(seatHome, plural);
  if (names.length > 0) assertRealDirectory(storeRoot, `${kind} store root`);

  const installRootInfo = lstatIfPresent(installRoot);
  if (installRootInfo?.isSymbolicLink() || (installRootInfo && !installRootInfo.isDirectory())) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `${kind} install root must be a real directory (the historical whole-store symlink requires explicit migration): ${installRoot}`,
    );
  }

  const installs: PlannedLink[] = names.map((name: string): PlannedLink => {
    const target = join(storeRoot, name);
    const targetInfo = lstatIfPresent(target);
    if (!targetInfo?.isDirectory() || targetInfo.isSymbolicLink()) {
      throw new FleetLaunchError(
        'COMPOSITION_FAILED',
        `${kind} store entry must be a real, non-symlink directory: ${target}`,
      );
    }
    assertContained(realpathSync(storeRoot), realpathSync(target), `${kind} store entry`);
    const link = join(installRoot, name);
    const linkInfo = lstatIfPresent(link);
    if (linkInfo && !linkInfo.isSymbolicLink()) {
      throw new FleetLaunchError(
        'COMPOSITION_FAILED',
        `real ${kind} directory occupies managed symlink path ${link}; refusing to delete it.`,
      );
    }
    return { kind, name, link, target: realpathSync(target) };
  });

  const desired = new Set(names);
  const prune: string[] = [];
  if (installRootInfo?.isDirectory()) {
    for (const entry of readdirSync(installRoot, { withFileTypes: true })) {
      const path = join(installRoot, entry.name);
      if (desired.has(entry.name)) continue;
      if (!entry.isSymbolicLink()) {
        // The harness writes its own metadata files (e.g. installed_plugins.json)
        // beside the managed links; only a real directory is an unmanaged entry
        // the pruner would orphan.
        if (entry.isDirectory()) {
          throw new FleetLaunchError(
            'COMPOSITION_FAILED',
            `real ${kind} directory occupies managed install root ${path}; refusing to prune it.`,
          );
        }
        continue;
      }
      prune.push(path);
    }
  }
  return { installs, prune };
}

function buildArgv(
  profile: FleetAgentLaunchProfile,
  seatHome: string,
  passthrough: string[],
): string[] {
  const argv: string[] = [profile.harness];
  if (profile.model) argv.push('--model', profile.model);
  if (profile.harness === 'pi') {
    for (const skill of profile.skills) argv.push('--skill', join(seatHome, 'skills', skill));
  }
  argv.push(...passthrough);
  return argv;
}

/** Resolve and validate the complete launch without changing the filesystem. */
export function resolveFleetLaunchComposition(
  name: string,
  roots: FleetLaunchRoots,
  passthrough: string[] = [],
): FleetLaunchComposition {
  if (!AGENT_NAME.test(name)) {
    throw new FleetLaunchError('PROFILE_INVALID', `invalid fleet agent name: ${name}`);
  }
  const agentDir = join(roots.userHome, 'fleet', 'agents', name);
  if (lstatIfPresent(agentDir) === undefined) {
    throw new FleetLaunchError(
      'AGENT_NOT_SCAFFOLDED',
      `no such fleet agent '${name}' — run: mosaic fleet agent new ${name}`,
    );
  }
  assertRealDirectory(agentDir, 'fleet agent directory');
  const profilePath = join(agentDir, 'profile.json');
  const profileInfo = lstatIfPresent(profilePath);
  if (!profileInfo?.isFile() || profileInfo.isSymbolicLink()) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `agent profile must be a real, non-symlink file: ${profilePath}`,
    );
  }
  const profile = parseFleetAgentProfile(readFileSync(profilePath, 'utf8'));
  const context: FleetHarnessContext = { agentDir };
  const seatHome = harnessHome(profile.harness, context);
  const seatHomeInfo = lstatIfPresent(seatHome);
  if (seatHomeInfo && (!seatHomeInfo.isDirectory() || seatHomeInfo.isSymbolicLink())) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `agent harness home must be a real, non-symlink directory: ${seatHome}`,
    );
  }
  const settingsOutput = join(seatHome, 'settings.json');
  const settingsSnapshot = join(agentDir, 'settings.generated.json');
  for (const [label, path] of [
    ['generated settings', settingsOutput],
    ['generated settings snapshot', settingsSnapshot],
  ] as const) {
    const info = lstatIfPresent(path);
    if (info && (!info.isFile() || info.isSymbolicLink())) {
      throw new FleetLaunchError(
        'COMPOSITION_FAILED',
        `${label} path must be a real file or absent: ${path}`,
      );
    }
  }
  const overlayPath = join(agentDir, profile.overlay ?? 'overlay.json');
  assertContained(agentDir, overlayPath, 'agent overlay');
  const layers: SettingsLayer[] = [
    readSettingsLayer(
      'system',
      join(roots.systemHome, 'runtime', profile.harness, 'settings.json'),
      true,
    ),
    readSettingsLayer(
      'user',
      join(roots.userHome, 'config', profile.harness, 'settings.json'),
      false,
    ),
    profile.overlay === undefined
      ? { name: 'agent', path: overlayPath, present: false, value: {} }
      : readSettingsLayer('agent', overlayPath, false),
  ];
  const merged = deepMergeSettings(...layers.map((layer) => layer.value));
  const credential = resolveCredential(profile, roots.userHome, seatHome);
  const plugins = resolveManagedLinks('plugin', profile.plugins, roots.userHome, seatHome);
  const skills = resolveManagedLinks('skill', profile.skills, roots.userHome, seatHome);
  const homeEnvName: Record<RuntimeName, string> = {
    claude: 'CLAUDE_CONFIG_DIR',
    pi: 'PI_CODING_AGENT_DIR',
    codex: 'CODEX_HOME',
    opencode: 'XDG_CONFIG_HOME',
  };
  const env: Record<string, string> = {
    ...profile.env,
    [homeEnvName[profile.harness]]: seatHome,
    MOSAIC_AGENT_NAME: name,
  };
  return {
    name,
    profilePath,
    profile,
    agentDir,
    seatHome,
    settings: {
      layers,
      merged,
      output: settingsOutput,
      snapshot: settingsSnapshot,
    },
    ...credential,
    installs: [...plugins.installs, ...skills.installs],
    prune: [...plugins.prune, ...skills.prune],
    env,
    argv: buildArgv(profile, seatHome, passthrough),
  };
}

function ensureSymlink(link: string, target: string): void {
  const info = lstatIfPresent(link);
  if (info?.isSymbolicLink()) {
    const current = resolve(dirname(link), readlinkSync(link));
    if (current === target) return;
    rmSync(link);
  } else if (info) {
    throw new FleetLaunchError(
      'COMPOSITION_FAILED',
      `real object occupies managed symlink path ${link}; refusing to delete it.`,
    );
  }
  mkdirSync(dirname(link), { recursive: true });
  symlinkSync(target, link, 'file');
}

function canonicalJson(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(canonicalJson);
  if (!isPlainObject(value)) return value;
  return Object.fromEntries(
    Object.keys(value)
      .sort((left, right) => left.localeCompare(right, 'en'))
      .map((key) => [key, canonicalJson(value[key])]),
  );
}

/** Apply a previously resolved plan. No caller should apply a dry-run plan. */
export function applyFleetLaunchComposition(plan: FleetLaunchComposition): void {
  mkdirSync(plan.seatHome, { recursive: true });
  const settings = `${JSON.stringify(canonicalJson(plan.settings.merged), null, 2)}\n`;
  writeFileSync(plan.settings.output, settings, { mode: 0o600 });
  writeFileSync(plan.settings.snapshot, settings, { mode: 0o600 });
  ensureSymlink(plan.credential.link, plan.credential.target);
  for (const path of plan.prune) {
    const info = lstatIfPresent(path);
    if (info?.isSymbolicLink()) rmSync(path);
    else if (info) {
      throw new FleetLaunchError(
        'COMPOSITION_FAILED',
        `real object replaced managed symlink before prune: ${path}`,
      );
    }
  }
  for (const install of plan.installs) ensureSymlink(install.link, install.target);
}

/** Stable, auditable text representation used by --dry-run and snapshot tests. */
export function formatFleetLaunchDryRun(plan: FleetLaunchComposition): string {
  const lines = [
    `mosaic fleet launch ${plan.name} --dry-run`,
    `profile: ${plan.profilePath} (schema ${plan.profile.schema})`,
    `harness: ${plan.profile.harness}`,
    `seat-home: ${plan.seatHome}`,
    'settings sources:',
  ];
  for (const layer of plan.settings.layers) {
    lines.push(`  ${layer.name}: ${layer.path}${layer.present ? '' : ' (absent)'}`);
  }
  lines.push(`  output: ${plan.settings.output}`);
  lines.push(`  snapshot: ${plan.settings.snapshot}`);
  lines.push('merged settings:');
  lines.push(JSON.stringify(canonicalJson(plan.settings.merged), null, 2));
  lines.push(`bundle: ${plan.bundle.display}`);
  lines.push('symlinks:');
  lines.push(`  credentials: ${plan.credential.link} -> ${plan.credential.target}`);
  for (const install of plan.installs) {
    lines.push(`  ${install.kind} ${install.name}: ${install.link} -> ${install.target}`);
  }
  for (const path of plan.prune) lines.push(`  prune: ${path}`);
  lines.push('declared env:');
  for (const key of Object.keys(plan.env).sort()) lines.push(`  ${key}=${plan.env[key]}`);
  lines.push(`argv: ${JSON.stringify(plan.argv)}`);
  return lines.join('\n');
}

/** Register `mosaic fleet launch <name> [--dry-run]` on the fleet control plane. */
export function registerFleetLaunchCommand(
  fleetCommand: Command,
  systemHomeFor: () => string,
  deps: FleetLaunchCommandDeps = {},
): Command {
  return fleetCommand
    .command('launch <name>')
    .description('Compose and launch one per-agent harness home')
    .option('--dry-run', 'Print the fully resolved composition without writing or launching')
    .allowUnknownOption(true)
    .allowExcessArguments(true)
    .action((name: string, opts: { dryRun?: boolean }, command: Command): void => {
      try {
        const userHome = deps.userHome ?? defaultFleetDataHome();
        const passthrough = command.args.slice(1);
        const plan = resolveFleetLaunchComposition(
          name,
          { systemHome: systemHomeFor(), userHome },
          passthrough,
        );
        if (opts.dryRun === true) {
          process.stdout.write(`${formatFleetLaunchDryRun(plan)}\n`);
          return;
        }
        applyFleetLaunchComposition(plan);
        console.log(`[mosaic] bundle: ${plan.bundle.display}`);
        const launcher = deps.launcher ?? launchFleetRuntime;
        launcher(plan.profile.harness, plan.argv.slice(1), plan.env, { agentDir: plan.agentDir });
      } catch (error: unknown) {
        process.exitCode = 1;
        const code = error instanceof FleetLaunchError ? `${error.code}: ` : '';
        const message = error instanceof Error ? error.message : String(error);
        process.stderr.write(`mosaic fleet launch failed: ${code}${message}\n`);
      }
    });
}
