import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { platform } from 'node:os';
import type { WizardPrompter } from '../prompter/interface.js';
import type { ConfigService } from '../config/config-service.js';
import type { WizardState } from '../types.js';
import { getShellProfilePath } from '../platform/detect.js';

function linkRuntimeAssets(mosaicHome: string, skipClaudeHooks: boolean): void {
  const script = join(mosaicHome, 'bin', 'mosaic-link-runtime-assets');
  if (existsSync(script)) {
    try {
      spawnSync('bash', [script], {
        timeout: 30000,
        stdio: 'pipe',
        env: {
          ...process.env,
          ...(skipClaudeHooks ? { MOSAIC_SKIP_CLAUDE_HOOKS: '1' } : {}),
        },
      });
    } catch {
      // Non-fatal: wizard continues
    }
  }
}

interface SyncSkillsResult {
  success: boolean;
  installedCount: number;
  failureReason?: string;
}

/**
 * Sync skills from the catalog and link only the user-selected subset.
 *
 * When `selectedSkills` is non-empty the script receives the list via
 * `MOSAIC_INSTALL_SKILLS` (colon-separated) so it can skip unlisted skills
 * during the linking phase.  An empty selection is a no-op.
 *
 * Failure modes surfaced here:
 *   - Script not found → tells the user explicitly
 *   - Script exits non-zero → stderr is captured and reported
 *   - Catalog directory missing → detected before exec, reported clearly
 */
function syncSkills(mosaicHome: string, selectedSkills: string[]): SyncSkillsResult {
  if (selectedSkills.length === 0) {
    return { success: true, installedCount: 0 };
  }

  const script = join(mosaicHome, 'bin', 'mosaic-sync-skills');
  if (!existsSync(script)) {
    return {
      success: false,
      installedCount: 0,
      failureReason: `Skills sync script not found at ${script} — run 'mosaic sync' after installation.`,
    };
  }

  try {
    const result = spawnSync('bash', [script], {
      timeout: 60000,
      stdio: 'pipe',
      encoding: 'utf-8',
      env: {
        ...process.env,
        MOSAIC_HOME: mosaicHome,
        MOSAIC_INSTALL_SKILLS: selectedSkills.join(':'),
      },
    });

    if (result.status !== 0) {
      const stderr = (result.stderr ?? '').trim();
      return {
        success: false,
        installedCount: 0,
        failureReason: stderr
          ? `Skills sync failed: ${stderr}`
          : `Skills sync script exited with code ${(result.status ?? 'unknown').toString()}`,
      };
    }

    return { success: true, installedCount: selectedSkills.length };
  } catch (err) {
    return {
      success: false,
      installedCount: 0,
      failureReason: `Skills sync threw: ${err instanceof Error ? err.message : String(err)}`,
    };
  }
}

interface DoctorResult {
  warnings: number;
  output: string;
}

function runDoctor(mosaicHome: string): DoctorResult {
  const script = join(mosaicHome, 'bin', 'mosaic-doctor');
  if (!existsSync(script)) {
    return { warnings: 0, output: 'mosaic-doctor not found' };
  }

  try {
    const result = spawnSync('bash', [script], {
      timeout: 30000,
      encoding: 'utf-8',
      stdio: 'pipe',
    });
    const output = result.stdout ?? '';
    const warnings = (output.match(/WARN/g) ?? []).length;
    return { warnings, output };
  } catch {
    return { warnings: 1, output: 'Doctor check failed' };
  }
}

type PathAction = 'already' | 'added' | 'skipped';

function setupPath(mosaicHome: string, _p: WizardPrompter): PathAction {
  const binDir = join(mosaicHome, 'bin');
  const currentPath = process.env['PATH'] ?? '';

  if (currentPath.includes(binDir)) {
    return 'already';
  }

  const profilePath = getShellProfilePath();
  if (!profilePath) return 'skipped';

  const isWindows = platform() === 'win32';
  const exportLine = isWindows
    ? `\n# Mosaic\n$env:Path = "${binDir};$env:Path"\n`
    : `\n# Mosaic\nexport PATH="${binDir}:$PATH"\n`;

  // Check if already in profile
  if (existsSync(profilePath)) {
    const content = readFileSync(profilePath, 'utf-8');
    if (content.includes(binDir)) {
      return 'already';
    }
  }

  try {
    appendFileSync(profilePath, exportLine, 'utf-8');
    return 'added';
  } catch {
    return 'skipped';
  }
}

export async function finalizeStage(
  p: WizardPrompter,
  state: WizardState,
  config: ConfigService,
): Promise<void> {
  p.separator();

  const spin = p.spinner();

  // 1. Sync framework files (before config writes so identity files aren't overwritten)
  spin.update('Syncing framework files...');
  await config.syncFramework(state.installAction);

  // 2. Write config files (after sync so they aren't overwritten by source templates)
  if (state.installAction !== 'keep') {
    spin.update('Writing configuration files...');
    await config.writeSoul(state.soul);
    await config.writeUser(state.user);
    await config.writeTools(state.tools);
  }

  // 3. Link runtime assets
  // Honor the hooks-preview decision: when the user declined hooks, pass
  // MOSAIC_SKIP_CLAUDE_HOOKS=1 to the linker so hooks-config.json is not
  // copied into ~/.claude/ while still linking the other runtime files.
  spin.update('Linking runtime assets...');
  const skipClaudeHooks = state.hooks?.accepted === false;
  linkRuntimeAssets(state.mosaicHome, skipClaudeHooks);

  // 4. Sync skills (only installs the user-selected subset)
  let skillsResult: SyncSkillsResult = { success: true, installedCount: 0 };
  if (state.selectedSkills.length > 0) {
    spin.update(`Installing ${state.selectedSkills.length.toString()} selected skill(s)...`);
    skillsResult = syncSkills(state.mosaicHome, state.selectedSkills);
  }

  // 5. Run doctor
  spin.update('Running health audit...');
  const doctorResult = runDoctor(state.mosaicHome);

  spin.stop('Installation complete');

  // Report skill install failure clearly (non-fatal but user should know)
  if (!skillsResult.success && skillsResult.failureReason) {
    p.warn(skillsResult.failureReason);
    p.warn("Run 'mosaic sync' manually after installation to install skills.");
  }

  // 6. PATH setup
  const pathAction = setupPath(state.mosaicHome, p);

  // 7. Summary
  const skillsSummary = skillsResult.success
    ? skillsResult.installedCount > 0
      ? `${skillsResult.installedCount.toString()} installed`
      : 'none selected'
    : `install failed — ${skillsResult.failureReason ?? 'unknown error'}`;

  const summary: string[] = [
    `Agent: ${state.soul.agentName ?? 'Assistant'}`,
    `Style: ${state.soul.communicationStyle ?? 'direct'}`,
    `Runtimes: ${state.runtimes.detected.join(', ') || 'none detected'}`,
    `Skills: ${skillsSummary}`,
    `Config: ${state.mosaicHome}`,
  ];

  if (doctorResult.warnings > 0) {
    summary.push(
      `Health: ${doctorResult.warnings.toString()} warning(s) — run 'mosaic doctor' for details`,
    );
  } else {
    summary.push('Health: all checks passed');
  }

  p.note(summary.join('\n'), 'Installation Summary');

  // 8. Next steps
  const nextSteps: string[] = [];
  if (pathAction === 'added') {
    const profilePath = getShellProfilePath();
    nextSteps.push(`Reload shell: source ${profilePath ?? '~/.profile'}`);
  }
  if (state.runtimes.detected.length === 0) {
    nextSteps.push('Install at least one runtime (claude, codex, or opencode)');
  }
  nextSteps.push("Launch with 'mosaic claude' (or codex/opencode)");
  nextSteps.push('Edit identity files directly in ~/.config/mosaic/ for fine-tuning');

  p.note(nextSteps.map((s, i) => `${(i + 1).toString()}. ${s}`).join('\n'), 'Next Steps');

  p.outro('Mosaic is ready.');
}
