/**
 * Update Command
 *
 * Refreshes OpenSpec skills and commands for configured tools.
 * Supports profile-aware updates, delivery changes, migration, and smart update detection.
 */
import path from 'path';
import chalk from 'chalk';
import ora from 'ora';
import * as fs from 'fs';
import { createRequire } from 'module';
import { FileSystemUtils } from '../utils/file-system.js';
import { transformToHyphenCommands } from '../utils/command-references.js';
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
import { isInteractive } from '../utils/interactive.js';
import { getGlobalConfig } from './global-config.js';
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import { WORKFLOW_TO_SKILL_DIR, getCommandConfiguredTools, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, } from './migration.js';
const require = createRequire(import.meta.url);
const { version: OPENSPEC_VERSION } = require('../../package.json');
/**
 * Scans installed workflow artifacts (skills and managed commands) across all configured tools.
 * Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
 *
 * Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
 */
export function scanInstalledWorkflows(projectPath, toolIds) {
    const tools = toolIds
        .map((id) => AI_TOOLS.find((t) => t.value === id))
        .filter((t) => t != null);
    return scanInstalledWorkflowsShared(projectPath, tools);
}
export class UpdateCommand {
    force;
    constructor(options = {}) {
        this.force = options.force ?? false;
    }
    async execute(projectPath) {
        const resolvedProjectPath = path.resolve(projectPath);
        const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
        // 1. Check openspec directory exists
        if (!await FileSystemUtils.directoryExists(openspecPath)) {
            throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
        }
        // 2. Perform one-time migration if needed before any legacy upgrade generation.
        // Use detected tool directories to preserve existing opsx skills/commands.
        const detectedTools = getAvailableTools(resolvedProjectPath);
        migrateIfNeededShared(resolvedProjectPath, detectedTools);
        // 3. Read global config for profile/delivery
        const globalConfig = getGlobalConfig();
        const profile = globalConfig.profile ?? 'core';
        const delivery = globalConfig.delivery ?? 'both';
        const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
        const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
        const shouldGenerateSkills = delivery !== 'commands';
        const shouldGenerateCommands = delivery !== 'skills';
        // 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
        const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
        // 5. Find configured tools
        const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
        if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
            console.log(chalk.yellow('No configured tools found.'));
            console.log(chalk.dim('Run "openspec init" to set up tools.'));
            return;
        }
        // 6. Check version status for all configured tools
        const commandConfiguredTools = getCommandConfiguredTools(resolvedProjectPath);
        const commandConfiguredSet = new Set(commandConfiguredTools);
        const toolStatuses = configuredTools.map((toolId) => {
            const status = getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION);
            if (!status.configured && commandConfiguredSet.has(toolId)) {
                return { ...status, configured: true };
            }
            return status;
        });
        const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
        // 7. Smart update detection
        const toolsNeedingVersionUpdate = toolStatuses
            .filter((s) => s.needsUpdate)
            .map((s) => s.toolId);
        const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
        const toolsToUpdateSet = new Set([
            ...toolsNeedingVersionUpdate,
            ...toolsNeedingConfigSync,
        ]);
        const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
        if (!this.force && toolsToUpdateSet.size === 0) {
            // All tools are up to date
            this.displayUpToDateMessage(toolStatuses);
            // Still check for new tool directories and extra workflows
            this.detectNewTools(resolvedProjectPath, configuredTools);
            this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
            return;
        }
        // 8. Display update plan
        if (this.force) {
            console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
        }
        else {
            this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
        }
        console.log();
        // 9. Determine what to generate based on delivery
        const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
        const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
        // 10. Update tools (all if force, otherwise only those needing update)
        const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
        const updatedTools = [];
        const failedTools = [];
        let removedCommandCount = 0;
        let removedSkillCount = 0;
        let removedDeselectedCommandCount = 0;
        let removedDeselectedSkillCount = 0;
        for (const toolId of toolsToUpdate) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            if (!tool?.skillsDir)
                continue;
            const spinner = ora(`Updating ${tool.name}...`).start();
            try {
                const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
                // Generate skill files if delivery includes skills
                if (shouldGenerateSkills) {
                    for (const { template, dirName } of skillTemplates) {
                        const skillDir = path.join(skillsDir, dirName);
                        const skillFile = path.join(skillDir, 'SKILL.md');
                        // Use hyphen-based command references for OpenCode
                        const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
                        const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
                        await FileSystemUtils.writeFile(skillFile, skillContent);
                    }
                    removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows);
                }
                // Delete skill directories if delivery is commands-only
                if (!shouldGenerateSkills) {
                    removedSkillCount += await this.removeSkillDirs(skillsDir);
                }
                // Generate commands if delivery includes commands
                if (shouldGenerateCommands) {
                    const adapter = CommandAdapterRegistry.get(tool.value);
                    if (adapter) {
                        const generatedCommands = generateCommands(commandContents, adapter);
                        for (const cmd of generatedCommands) {
                            const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
                            await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
                        }
                        removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, desiredWorkflows);
                    }
                }
                // Delete command files if delivery is skills-only
                if (!shouldGenerateCommands) {
                    removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
                }
                spinner.succeed(`Updated ${tool.name}`);
                updatedTools.push(tool.name);
            }
            catch (error) {
                spinner.fail(`Failed to update ${tool.name}`);
                failedTools.push({
                    name: tool.name,
                    error: error instanceof Error ? error.message : String(error)
                });
            }
        }
        // 11. Summary
        console.log();
        if (updatedTools.length > 0) {
            console.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
        }
        if (failedTools.length > 0) {
            console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
        }
        if (removedCommandCount > 0) {
            console.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
        }
        if (removedSkillCount > 0) {
            console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
        }
        if (removedDeselectedCommandCount > 0) {
            console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
        }
        if (removedDeselectedSkillCount > 0) {
            console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
        }
        // 12. Show onboarding message for newly configured tools from legacy upgrade
        if (newlyConfiguredTools.length > 0) {
            console.log();
            console.log(chalk.bold('Getting started:'));
            console.log('  /opsx:new       Start a new change');
            console.log('  /opsx:continue  Create the next artifact');
            console.log('  /opsx:apply     Implement tasks');
            console.log();
            console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
        }
        const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
        // 13. Detect new tool directories not currently configured
        this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
        // 14. Display note about extra workflows not in profile
        this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
        // 15. List affected tools
        if (updatedTools.length > 0) {
            const toolDisplayNames = updatedTools;
            console.log(chalk.dim(`Tools: ${toolDisplayNames.join(', ')}`));
        }
        console.log();
        console.log(chalk.dim('Restart your IDE for changes to take effect.'));
    }
    /**
     * Display message when all tools are up to date.
     */
    displayUpToDateMessage(toolStatuses) {
        const toolNames = toolStatuses.map((s) => s.toolId);
        console.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
        console.log(chalk.dim(`  Tools: ${toolNames.join(', ')}`));
        console.log();
        console.log(chalk.dim('Use --force to refresh files anyway.'));
    }
    /**
     * Display the update plan showing which tools need updating.
     */
    displayUpdatePlan(toolsToUpdate, statusByTool, upToDate) {
        const updates = toolsToUpdate.map((toolId) => {
            const status = statusByTool.get(toolId);
            if (status?.needsUpdate) {
                const fromVersion = status.generatedByVersion ?? 'unknown';
                return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
            }
            return `${toolId} (config sync)`;
        });
        console.log(`Updating ${toolsToUpdate.length} tool(s): ${updates.join(', ')}`);
        if (upToDate.length > 0) {
            const upToDateNames = upToDate.map((s) => s.toolId);
            console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
        }
    }
    /**
     * Detects new tool directories that aren't currently configured and displays a hint.
     */
    detectNewTools(projectPath, configuredTools) {
        const availableTools = getAvailableTools(projectPath);
        const configuredSet = new Set(configuredTools);
        const newTools = availableTools.filter((t) => !configuredSet.has(t.value));
        if (newTools.length > 0) {
            const newToolNames = newTools.map((tool) => tool.name);
            const isSingleTool = newToolNames.length === 1;
            const toolNoun = isSingleTool ? 'tool' : 'tools';
            const pronoun = isSingleTool ? 'it' : 'them';
            console.log();
            console.log(chalk.yellow(`Detected new ${toolNoun}: ${newToolNames.join(', ')}. Run 'openspec init' to add ${pronoun}.`));
        }
    }
    /**
     * Displays a note about extra workflows installed that aren't in the current profile.
     */
    displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) {
        const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools);
        const profileSet = new Set(profileWorkflows);
        const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w));
        if (extraWorkflows.length > 0) {
            console.log(chalk.dim(`Note: ${extraWorkflows.length} extra workflows not in profile (use \`openspec config profile\` to manage)`));
        }
    }
    /**
     * Removes skill directories for workflows when delivery changed to commands-only.
     * Returns the number of directories removed.
     */
    async removeSkillDirs(skillsDir) {
        let removed = 0;
        for (const workflow of ALL_WORKFLOWS) {
            const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
            if (!dirName)
                continue;
            const skillDir = path.join(skillsDir, dirName);
            try {
                if (fs.existsSync(skillDir)) {
                    await fs.promises.rm(skillDir, { recursive: true, force: true });
                    removed++;
                }
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Removes skill directories for workflows that are no longer selected in the active profile.
     * Returns the number of directories removed.
     */
    async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
        const desiredSet = new Set(desiredWorkflows);
        let removed = 0;
        for (const workflow of ALL_WORKFLOWS) {
            if (desiredSet.has(workflow))
                continue;
            const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
            if (!dirName)
                continue;
            const skillDir = path.join(skillsDir, dirName);
            try {
                if (fs.existsSync(skillDir)) {
                    await fs.promises.rm(skillDir, { recursive: true, force: true });
                    removed++;
                }
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Removes command files for workflows when delivery changed to skills-only.
     * Returns the number of files removed.
     */
    async removeCommandFiles(projectPath, toolId) {
        let removed = 0;
        const adapter = CommandAdapterRegistry.get(toolId);
        if (!adapter)
            return 0;
        for (const workflow of ALL_WORKFLOWS) {
            const cmdPath = adapter.getFilePath(workflow);
            const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
            try {
                if (fs.existsSync(fullPath)) {
                    await fs.promises.unlink(fullPath);
                    removed++;
                }
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Removes command files for workflows that are no longer selected in the active profile.
     * Returns the number of files removed.
     */
    async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) {
        let removed = 0;
        const adapter = CommandAdapterRegistry.get(toolId);
        if (!adapter)
            return 0;
        const desiredSet = new Set(desiredWorkflows);
        for (const workflow of ALL_WORKFLOWS) {
            if (desiredSet.has(workflow))
                continue;
            const cmdPath = adapter.getFilePath(workflow);
            const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
            try {
                if (fs.existsSync(fullPath)) {
                    await fs.promises.unlink(fullPath);
                    removed++;
                }
            }
            catch {
                // Ignore errors
            }
        }
        return removed;
    }
    /**
     * Detect and handle legacy OpenSpec artifacts.
     * Unlike init, update warns but continues if legacy files found in non-interactive mode.
     * Returns array of tool IDs that were newly configured during legacy upgrade.
     */
    async handleLegacyCleanup(projectPath, desiredWorkflows, delivery) {
        // Detect legacy artifacts
        const detection = await detectLegacyArtifacts(projectPath);
        if (!detection.hasLegacyArtifacts) {
            return []; // No legacy artifacts found
        }
        // Show what was detected
        console.log();
        console.log(formatDetectionSummary(detection));
        console.log();
        const canPrompt = isInteractive();
        if (this.force) {
            // --force flag: proceed with cleanup automatically
            await this.performLegacyCleanup(projectPath, detection);
            // Then upgrade legacy tools to new skills
            return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
        }
        if (!canPrompt) {
            // Non-interactive mode without --force: warn and continue
            // (Unlike init, update doesn't abort - user may just want to update skills)
            console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
            console.log();
            return [];
        }
        // Interactive mode: prompt for confirmation
        const { confirm } = await import('@inquirer/prompts');
        const shouldCleanup = await confirm({
            message: 'Upgrade and clean up legacy files?',
            default: true,
        });
        if (shouldCleanup) {
            await this.performLegacyCleanup(projectPath, detection);
            // Then upgrade legacy tools to new skills
            return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
        }
        else {
            console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
            console.log();
            return [];
        }
    }
    /**
     * Perform cleanup of legacy artifacts.
     */
    async performLegacyCleanup(projectPath, detection) {
        const spinner = ora('Cleaning up legacy files...').start();
        const result = await cleanupLegacyArtifacts(projectPath, detection);
        spinner.succeed('Legacy files cleaned up');
        const summary = formatCleanupSummary(result);
        if (summary) {
            console.log();
            console.log(summary);
        }
        console.log();
    }
    /**
     * Upgrade legacy tools to new skills system.
     * Returns array of tool IDs that were newly configured.
     */
    async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
        // Get tools that had legacy artifacts
        const legacyTools = getToolsFromLegacyArtifacts(detection);
        if (legacyTools.length === 0) {
            return [];
        }
        // Get currently configured tools
        const configuredTools = getConfiguredToolsForProfileSync(projectPath);
        const configuredSet = new Set(configuredTools);
        // Filter to tools that aren't already configured
        const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
        if (unconfiguredLegacyTools.length === 0) {
            return [];
        }
        // Get valid tools (those with skillsDir)
        const validToolIds = new Set(getToolsWithSkillsDir());
        const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
        if (validUnconfiguredTools.length === 0) {
            return [];
        }
        // Show what tools were detected from legacy artifacts
        console.log(chalk.bold('Tools detected from legacy artifacts:'));
        for (const toolId of validUnconfiguredTools) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            console.log(`  • ${tool?.name || toolId}`);
        }
        console.log();
        let selectedTools;
        if (this.force || !canPrompt) {
            // Non-interactive with --force: auto-select detected tools
            selectedTools = validUnconfiguredTools;
            console.log(`Setting up skills for: ${selectedTools.join(', ')}`);
        }
        else {
            // Interactive mode: prompt for tool selection with detected tools pre-selected
            const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
            const sortedChoices = validUnconfiguredTools.map((toolId) => {
                const tool = AI_TOOLS.find((t) => t.value === toolId);
                return {
                    name: tool?.name || toolId,
                    value: toolId,
                    configured: false,
                    preSelected: true, // Pre-select all detected legacy tools
                };
            });
            selectedTools = await searchableMultiSelect({
                message: 'Select tools to set up with the new skill system:',
                pageSize: 15,
                choices: sortedChoices,
                validate: (_selected) => true, // Allow empty selection (user can skip)
            });
            if (selectedTools.length === 0) {
                console.log(chalk.dim('Skipping tool setup.'));
                console.log();
                return [];
            }
        }
        // Create skills/commands for selected tools using effective profile+delivery.
        const newlyConfigured = [];
        const shouldGenerateSkills = delivery !== 'commands';
        const shouldGenerateCommands = delivery !== 'skills';
        const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
        const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
        for (const toolId of selectedTools) {
            const tool = AI_TOOLS.find((t) => t.value === toolId);
            if (!tool?.skillsDir)
                continue;
            const spinner = ora(`Setting up ${tool.name}...`).start();
            try {
                const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
                // Create skill files when delivery includes skills
                if (shouldGenerateSkills) {
                    for (const { template, dirName } of skillTemplates) {
                        const skillDir = path.join(skillsDir, dirName);
                        const skillFile = path.join(skillDir, 'SKILL.md');
                        // Use hyphen-based command references for OpenCode
                        const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
                        const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
                        await FileSystemUtils.writeFile(skillFile, skillContent);
                    }
                }
                // Create commands when delivery includes commands
                if (shouldGenerateCommands) {
                    const adapter = CommandAdapterRegistry.get(tool.value);
                    if (adapter) {
                        const generatedCommands = generateCommands(commandContents, adapter);
                        for (const cmd of generatedCommands) {
                            const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
                            await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
                        }
                    }
                }
                spinner.succeed(`Setup complete for ${tool.name}`);
                newlyConfigured.push(toolId);
            }
            catch (error) {
                spinner.fail(`Failed to set up ${tool.name}`);
                console.log(chalk.red(`  ${error instanceof Error ? error.message : String(error)}`));
            }
        }
        if (newlyConfigured.length > 0) {
            console.log();
        }
        return newlyConfigured;
    }
}
//# sourceMappingURL=update.js.map