import { execFile } from 'node:child_process';
import { chmod } from 'node:fs/promises';
import { join } from 'node:path';
import { buildCommandContext } from '../../../app/command-context.js';
import { applyOatCoreGitignore } from '../../init/gitignore.js';
import { copyDirWithStatus, copyFileWithStatus, } from '../../init/tools/shared/copy-helpers.js';
import { withScopeOption } from '../../shared/scope-option.js';
import { readGlobalOptions, resolveConcreteScopes, } from '../../shared/shared.utils.js';
import { autoSync, } from '../../tools/shared/auto-sync.js';
import { scanTools } from '../../tools/shared/scan-tools.js';
import { readOatConfig, writeOatConfig } from '../../../config/oat-config.js';
import { resolveAssetsRoot } from '../../../fs/assets.js';
import { fileExists } from '../../../fs/io.js';
import { resolveProjectRoot, resolveScopeRoot } from '../../../fs/paths.js';
import { Command } from 'commander';
import { updateTools, } from './update-tools.js';
const defaultDependencies = {
    scanTools,
    resolveScopeRoot: async (scope, cwd, home) => {
        if (scope === 'project')
            return resolveProjectRoot(cwd);
        return resolveScopeRoot(scope, cwd, home);
    },
    resolveAssetsRoot,
    copyDirWithStatus,
    copyFileWithStatus,
    fileExists,
    chmod,
    applyOatCoreGitignore,
};
export function buildSyncSubprocessArgs(entrypoint, execArgv, options) {
    // `--scope` is a per-command option on `sync` (not a global flag), so it must
    // come AFTER the `sync` subcommand token. `--cwd` remains a global flag and
    // can precede the subcommand.
    return [
        ...execArgv,
        entrypoint,
        '--cwd',
        options.cwd,
        'sync',
        '--scope',
        options.scope,
    ];
}
const defaultSyncDependencies = {
    runSync: async ({ scope, cwd }) => {
        await new Promise((resolve, reject) => {
            execFile(process.execPath, buildSyncSubprocessArgs(process.argv[1], process.execArgv, {
                cwd,
                scope,
            }), { cwd: process.cwd() }, (error) => {
                if (error)
                    reject(error);
                else
                    resolve();
            });
        });
    },
};
const VALID_PACKS = [
    'core',
    'ideas',
    'docs',
    'workflows',
    'utility',
    'project-management',
    'research',
    'brainstorm',
];
export function createToolsUpdateCommand(dependencies = defaultDependencies, syncDependencies = defaultSyncDependencies) {
    return withScopeOption(new Command('update'))
        .description('Update installed tools to bundled versions')
        .argument('[name]', 'Tool name to update')
        .option('--pack <pack>', 'Update all tools in a pack (core|ideas|docs|workflows|utility|project-management|research|brainstorm)')
        .option('--all', 'Update all outdated tools')
        .option('--dry-run', 'Preview updates without applying')
        .option('--no-sync', 'Skip auto-sync after update')
        .action(async (name, opts, command) => {
        const globalOptions = readGlobalOptions(command);
        const context = buildCommandContext(globalOptions);
        const { logger } = context;
        const targetResolution = resolveTarget(name, opts.pack, opts.all);
        if ('error' in targetResolution) {
            logger.error(targetResolution.error);
            process.exitCode = 1;
            return;
        }
        const target = targetResolution.target;
        const scopes = resolveConcreteScopes(context.scope);
        const dryRun = opts.dryRun ?? false;
        const result = await updateTools(target, scopes, context.cwd, context.home, dryRun, dependencies);
        const assetsRoot = dryRun ? null : await dependencies.resolveAssetsRoot();
        if (!dryRun &&
            shouldBackfillWorkflowGitignore(result) &&
            dependencies.applyOatCoreGitignore) {
            const repoRoot = await resolveProjectRoot(context.cwd);
            const gitignoreResult = await dependencies.applyOatCoreGitignore(repoRoot);
            if (gitignoreResult.action !== 'no-change') {
                const verb = gitignoreResult.action === 'created' ? 'Created' : 'Updated';
                logger.info(`${verb} .gitignore OAT core section (${gitignoreResult.entries.length} entries).`);
            }
            if (gitignoreResult.stateDashboardIndexAction === 'untracked') {
                logger.info('Untracked generated dashboard from git index: .oat/state.md.');
            }
        }
        // Refresh ~/.oat/docs/ when the core pack is explicitly updated or
        // reconciled through --all (D3 requirement).
        if (shouldRefreshCoreDocs(target, result) && assetsRoot) {
            const userRoot = await dependencies.resolveScopeRoot('user', context.cwd, context.home);
            const docsSource = join(assetsRoot, 'docs');
            const docsDestination = join(userRoot, '.oat', 'docs');
            await dependencies.copyDirWithStatus(docsSource, docsDestination, true);
        }
        if (assetsRoot && (target.kind === 'all' || target.kind === 'pack')) {
            const repoRoot = await resolveProjectRoot(context.cwd);
            const config = await readOatConfig(repoRoot);
            const installedPacks = new Set();
            const configScopes = resolveConcreteScopes('all');
            for (const scope of configScopes) {
                const scopeRoot = await dependencies.resolveScopeRoot(scope, context.cwd, context.home);
                const tools = await dependencies.scanTools({
                    scope,
                    scopeRoot,
                    assetsRoot,
                });
                for (const tool of tools) {
                    if (tool.pack !== 'custom') {
                        installedPacks.add(tool.pack);
                    }
                }
            }
            // Preserve unrelated shared config keys while rebuilding tools state
            // from the repo-wide installed-pack scan.
            await writeOatConfig(repoRoot, {
                ...config,
                tools: Object.fromEntries(VALID_PACKS.map((pack) => [pack, installedPacks.has(pack)])),
            });
        }
        if (result.notInstalled.length > 0) {
            if (context.json) {
                logger.json({ target: describeTarget(target), dryRun, result });
            }
            else {
                logger.error(`Tool '${result.notInstalled[0]}' not found.`);
            }
            process.exitCode = 1;
            return;
        }
        // Auto-sync after mutations (before output so sync errors are captured)
        if (result.updated.length > 0 && !dryRun && opts.sync !== false) {
            const affectedScopes = [...new Set(result.updated.map((t) => t.scope))];
            await autoSync(affectedScopes, context.cwd, context.home, logger, syncDependencies);
        }
        if (context.json) {
            logger.json({ target: describeTarget(target), dryRun, result });
            return;
        }
        if (result.updated.length > 0) {
            for (const tool of result.updated) {
                logger.success(formatUpdatedToolMessage(tool, dryRun));
            }
        }
        for (const tool of result.current) {
            logger.info(`Already current: ${tool.name} (${tool.version ?? '?'})`);
        }
        for (const tool of result.newer) {
            logger.info(`Newer than bundled: ${tool.name} (${tool.version ?? '?'} > ${tool.bundledVersion ?? '?'})`);
        }
        for (const tool of result.notBundled) {
            logger.info(`Not bundled (custom): ${tool.name}`);
        }
        for (const asset of result.assetRefreshes) {
            const action = asset.status === 'planned' ? 'Would refresh' : 'Refreshed';
            logger.info(`${action} ${asset.type}: ${asset.name} (${asset.scope} ${asset.pack} pack)`);
        }
        if (result.updated.length === 0 && result.assetRefreshes.length === 0) {
            logger.info('No tools to update.');
        }
    });
}
/**
 * @internal Exported for focused unit coverage. Current/newer workflow tools
 * intentionally trigger this path so older installs can repair a missing OAT
 * core .gitignore section even when their workflow pack is already current.
 */
export function shouldBackfillWorkflowGitignore(result) {
    return [...result.updated, ...result.current, ...result.newer].some((tool) => tool.scope === 'project' && tool.pack === 'workflows');
}
export function shouldRefreshCoreDocs(target, result) {
    if (target.kind === 'name')
        return false;
    if (target.kind === 'pack')
        return target.pack === 'core';
    return [...result.updated, ...result.current, ...result.newer].some((tool) => tool.pack === 'core');
}
export function formatUpdatedToolMessage(tool, dryRun) {
    if (tool.version === null) {
        return `${dryRun ? 'Would install' : 'Installed'}: ${tool.name}`;
    }
    return `${dryRun ? 'Would update' : 'Updated'}: ${tool.name} (${tool.version} -> ${tool.bundledVersion ?? '?'})`;
}
function resolveTarget(name, pack, all) {
    const specified = [name, pack, all].filter(Boolean).length;
    if (specified === 0) {
        return {
            error: 'Specify a tool name, --pack <pack>, or --all. To update all tools, run: oat tools update --all',
        };
    }
    if (specified > 1) {
        return {
            error: 'Specify exactly one update target: a tool name, --pack <pack>, or --all.',
        };
    }
    if (name)
        return { target: { kind: 'name', name } };
    if (pack) {
        if (!VALID_PACKS.includes(pack)) {
            return {
                error: `Invalid pack '${pack}'. Expected one of: ${VALID_PACKS.join(', ')}.`,
            };
        }
        return { target: { kind: 'pack', pack: pack } };
    }
    return { target: { kind: 'all' } };
}
function describeTarget(target) {
    switch (target.kind) {
        case 'name':
            return target.name;
        case 'pack':
            return `pack:${target.pack}`;
        case 'all':
            return 'all';
    }
}
