"use strict";
/**
 * Migration Manager
 *
 * Handles migration from legacy JSON config (v1) to unified YAML config (v2).
 * Features:
 * - Automatic backup before migration
 * - Rollback support
 * - Settings file reference preservation (*.settings.json)
 * - Cache file restructuring
 *
 * Design: Settings remain in *.settings.json files (matching Claude's pattern)
 * while config.yaml stores references to these files.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.autoMigrate = exports.rollback = exports.migrate = exports.getBackupDirectories = exports.needsProfileMigration = exports.loadMigrationCheckData = exports.needsMigration = exports.resolveManagedBackupPath = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const config_manager_1 = require("../utils/config-manager");
const helpers_1 = require("../utils/helpers");
const profile_compat_1 = require("../utils/profile-compat");
const unified_config_types_1 = require("./unified-config-types");
const provider_capabilities_1 = require("../cliproxy/provider-capabilities");
const unified_config_loader_1 = require("./unified-config-loader");
const account_context_1 = require("../auth/account-context");
const shared_resource_policy_1 = require("../auth/shared-resource-policy");
const ui_1 = require("../utils/ui");
const BACKUP_DIR_PREFIX = 'backup-v1-';
function resolveCanonicalPath(targetPath) {
    try {
        return fs.realpathSync.native(targetPath);
    }
    catch {
        return path.resolve(targetPath);
    }
}
function isPathWithinDirectory(candidatePath, rootPath) {
    const normalizedCandidate = path.resolve(candidatePath);
    const normalizedRoot = path.resolve(rootPath);
    const relative = path.relative(normalizedRoot, normalizedCandidate);
    return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
}
function resolveManagedBackupPath(backupPath) {
    const resolvedInput = path.resolve(backupPath);
    const baseName = path.basename(resolvedInput);
    if (!baseName.startsWith(BACKUP_DIR_PREFIX)) {
        return null;
    }
    try {
        const stats = fs.lstatSync(resolvedInput);
        if (!stats.isDirectory() || stats.isSymbolicLink()) {
            return null;
        }
    }
    catch {
        return null;
    }
    const canonicalCcsDir = resolveCanonicalPath((0, config_manager_1.getCcsDir)());
    const canonicalBackupPath = resolveCanonicalPath(resolvedInput);
    if (!isPathWithinDirectory(canonicalBackupPath, canonicalCcsDir)) {
        return null;
    }
    if (path.dirname(canonicalBackupPath) !== canonicalCcsDir) {
        return null;
    }
    return canonicalBackupPath;
}
exports.resolveManagedBackupPath = resolveManagedBackupPath;
/**
 * Check if migration from v1 to v2 is needed.
 */
function needsMigration() {
    const ccsDir = (0, config_manager_1.getCcsDir)();
    const hasOldConfig = fs.existsSync(path.join(ccsDir, 'config.json'));
    const hasNewConfig = (0, unified_config_loader_1.hasUnifiedConfig)();
    return hasOldConfig && !hasNewConfig;
}
exports.needsMigration = needsMigration;
/**
 * Load config files once for TOCTOU-safe migration operations.
 * Returns data that can be passed to migration functions.
 */
function loadMigrationCheckData() {
    const ccsDir = (0, config_manager_1.getCcsDir)();
    const configJsonPath = path.join(ccsDir, 'config.json');
    const legacyConfig = fs.existsSync(configJsonPath) ? readJsonSafe(configJsonPath) : null;
    const unifiedConfig = (0, unified_config_loader_1.loadUnifiedConfig)();
    // Determine if migration is needed
    let needsMigration = false;
    if (legacyConfig?.profiles && typeof legacyConfig.profiles === 'object' && unifiedConfig) {
        const legacyProfiles = legacyConfig.profiles;
        const unifiedProfiles = unifiedConfig.profiles ?? {};
        for (const profileName of Object.keys(legacyProfiles)) {
            const targetProfileName = (0, profile_compat_1.resolveAliasToCanonical)(profileName);
            if (!unifiedProfiles[targetProfileName]) {
                needsMigration = true;
                break;
            }
        }
    }
    return { legacyConfig, unifiedConfig, needsMigration };
}
exports.loadMigrationCheckData = loadMigrationCheckData;
/**
 * Check if there are legacy profiles that haven't been migrated to config.yaml.
 * This catches the case where config.yaml exists but is empty/missing profiles
 * that are still in config.json.
 *
 * Used by autoMigrate() to trigger inline migration when needed.
 * (Fix for issue #195 - GLM auth persistence regression)
 *
 * Note: For TOCTOU-safe operations, use loadMigrationCheckData() instead.
 */
function needsProfileMigration() {
    const data = loadMigrationCheckData();
    return data.needsMigration;
}
exports.needsProfileMigration = needsProfileMigration;
/**
 * Get list of backup directories.
 */
function getBackupDirectories() {
    const ccsDir = (0, config_manager_1.getCcsDir)();
    if (!fs.existsSync(ccsDir))
        return [];
    return fs
        .readdirSync(ccsDir)
        .filter((name) => name.startsWith(BACKUP_DIR_PREFIX))
        .map((name) => path.join(ccsDir, name))
        .filter((backupPath) => resolveManagedBackupPath(backupPath) !== null)
        .sort()
        .reverse(); // Most recent first
}
exports.getBackupDirectories = getBackupDirectories;
/**
 * Perform migration from v1 to v2 format.
 */
async function migrate(dryRun = false) {
    const ccsDir = (0, config_manager_1.getCcsDir)();
    const migratedFiles = [];
    const warnings = [];
    try {
        // 1. Create backup
        const timestamp = new Date().toISOString().split('T')[0];
        const backupDir = path.join(ccsDir, `${BACKUP_DIR_PREFIX}${timestamp}`);
        if (!dryRun) {
            await createBackup(ccsDir, backupDir);
        }
        // 2. Read old configs
        const oldConfig = readJsonSafe(path.join(ccsDir, 'config.json'));
        const oldProfiles = readJsonSafe(path.join(ccsDir, 'profiles.json'));
        // 3. Build unified config
        const unifiedConfig = (0, unified_config_types_1.createEmptyUnifiedConfig)();
        // Set default if exists
        if (oldProfiles?.default && typeof oldProfiles.default === 'string') {
            unifiedConfig.default = oldProfiles.default;
        }
        // 4. Migrate accounts from profiles.json
        if (oldProfiles?.profiles) {
            for (const [name, meta] of Object.entries(oldProfiles.profiles)) {
                const metadata = meta;
                const rawContextMode = metadata.context_mode;
                const rawContextGroup = metadata.context_group;
                const rawContinuityMode = metadata.continuity_mode;
                const contextMode = rawContextMode === 'shared' ? 'shared' : 'isolated';
                const continuityMode = contextMode === 'shared' && rawContinuityMode === 'deeper' ? 'deeper' : 'standard';
                let contextGroup;
                if (typeof rawContextGroup === 'string' && rawContextGroup.trim().length > 0) {
                    const normalizedGroup = (0, account_context_1.normalizeContextGroupName)(rawContextGroup);
                    if ((0, account_context_1.isValidContextGroupName)(normalizedGroup)) {
                        contextGroup = normalizedGroup;
                    }
                    else {
                        warnings.push(`Skipped invalid context group for account "${name}": "${rawContextGroup}" (fallback to default shared group)`);
                    }
                }
                const account = {
                    created: metadata.created || new Date().toISOString(),
                    last_used: metadata.last_used || null,
                    context_mode: contextMode,
                    context_group: contextMode === 'shared' ? contextGroup : undefined,
                    continuity_mode: contextMode === 'shared' ? continuityMode : undefined,
                };
                const resourcePolicy = (0, shared_resource_policy_1.resolveSharedResourcePolicy)({
                    shared_resource_mode: metadata.shared_resource_mode,
                    bare: metadata.bare,
                });
                if (resourcePolicy.mode === 'profile-local') {
                    account.shared_resource_mode = 'profile-local';
                    account.bare = true;
                }
                else if ((0, shared_resource_policy_1.isSharedResourceMode)(metadata.shared_resource_mode)) {
                    account.shared_resource_mode = 'shared';
                }
                unifiedConfig.accounts[name] = account;
            }
            migratedFiles.push('profiles.json → config.yaml.accounts');
        }
        // 5. Migrate CLIProxy variants from config.json
        if (oldConfig?.cliproxy) {
            for (const [name, variantData] of Object.entries(oldConfig.cliproxy)) {
                const oldVariant = variantData;
                const variant = {
                    provider: oldVariant.provider,
                };
                if (oldVariant.account) {
                    variant.account = oldVariant.account;
                }
                // Keep reference to existing settings file
                if (oldVariant.settings) {
                    variant.settings = oldVariant.settings;
                }
                unifiedConfig.cliproxy.variants[name] = variant;
            }
            migratedFiles.push('config.json.cliproxy → config.yaml.cliproxy.variants');
        }
        // 6. Migrate API profiles from config.json
        // Keep settings in *.settings.json files (matching Claude's ~/.claude/settings.json pattern)
        // config.yaml only stores reference to the settings file
        if (oldConfig?.profiles) {
            for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
                const sourceName = name.trim();
                const targetName = (0, profile_compat_1.resolveAliasToCanonical)(sourceName);
                const pathStr = settingsPath;
                const expandedPath = (0, helpers_1.expandPath)(pathStr);
                const canonicalEntryValue = oldConfig.profiles[targetName];
                const canonicalPathFromLegacyConfig = sourceName !== targetName && typeof canonicalEntryValue === 'string'
                    ? canonicalEntryValue
                    : undefined;
                // Deterministic priority: explicit canonical profile wins over legacy alias rename.
                if (canonicalPathFromLegacyConfig !== undefined) {
                    if (canonicalPathFromLegacyConfig !== pathStr) {
                        warnings.push(`Skipped ${sourceName}: canonical profile "${targetName}" exists in config.json with different settings (${canonicalPathFromLegacyConfig})`);
                    }
                    continue;
                }
                // Verify settings file exists
                if (!fs.existsSync(expandedPath)) {
                    warnings.push(`Skipped ${sourceName}: settings file not found at ${pathStr}`);
                    continue;
                }
                if (unifiedConfig.profiles[targetName]) {
                    const existing = unifiedConfig.profiles[targetName].settings;
                    if (existing !== pathStr) {
                        warnings.push(`Skipped ${sourceName}: target profile "${targetName}" already exists with different settings (${existing})`);
                    }
                    continue;
                }
                // Store reference to settings file (keep using ~ for portability)
                const profile = {
                    type: 'api',
                    settings: pathStr,
                };
                unifiedConfig.profiles[targetName] = profile;
                migratedFiles.push(`config.json.profiles.${sourceName} → config.yaml.profiles.${targetName} (settings: ${pathStr})`);
                if (targetName !== sourceName) {
                    warnings.push(`Renamed legacy API profile "${sourceName}" to "${targetName}" (ccs kimi API profile is now ccs km)`);
                }
            }
        }
        // 6b. Migrate built-in CLIProxy OAuth profile settings (gemini, codex, agy, qwen, iflow)
        // Keep settings in *.settings.json files - only record reference in config.yaml
        // This matches Claude's ~/.claude/settings.json pattern for user familiarity
        const builtInProviders = [...provider_capabilities_1.CLIPROXY_PROVIDER_IDS];
        for (const provider of builtInProviders) {
            const settingsFile = `${provider}.settings.json`;
            const settingsPath = path.join(ccsDir, settingsFile);
            if (fs.existsSync(settingsPath)) {
                // Create variant with reference to settings file
                const variant = {
                    provider: provider,
                    settings: `~/.ccs/${settingsFile}`,
                };
                unifiedConfig.cliproxy.variants[provider] = variant;
                migratedFiles.push(`${settingsFile} → config.yaml.cliproxy.variants.${provider}`);
            }
        }
        // 7. Write new config FIRST (before moving files - H8 fix)
        // Note: Settings remain in *.settings.json files, config.yaml only stores references
        if (!dryRun) {
            (0, unified_config_loader_1.saveUnifiedConfig)(unifiedConfig);
        }
        // 8. Migrate cache files AFTER config is saved (H8: prevents inconsistent state)
        if (!dryRun) {
            const cacheDir = path.join(ccsDir, 'cache');
            if (!fs.existsSync(cacheDir)) {
                fs.mkdirSync(cacheDir, { recursive: true });
            }
            if (moveIfExists(path.join(ccsDir, 'usage-cache.json'), path.join(cacheDir, 'usage.json'))) {
                migratedFiles.push('usage-cache.json → cache/usage.json');
            }
            if (moveIfExists(path.join(ccsDir, 'update-check.json'), path.join(cacheDir, 'update-check.json'))) {
                migratedFiles.push('update-check.json → cache/update-check.json');
            }
        }
        return {
            success: true,
            backupPath: dryRun ? undefined : backupDir,
            migratedFiles,
            warnings,
        };
    }
    catch (err) {
        return {
            success: false,
            error: err instanceof Error ? err.message : 'Unknown error',
            migratedFiles,
            warnings,
        };
    }
}
exports.migrate = migrate;
/**
 * Rollback migration by restoring from backup.
 */
async function rollback(backupPath) {
    const ccsDir = (0, config_manager_1.getCcsDir)();
    const managedBackupPath = resolveManagedBackupPath(backupPath);
    if (!managedBackupPath) {
        console.error(`[X] Invalid backup path: ${backupPath}`);
        return false;
    }
    try {
        // Remove new config files
        const configYaml = path.join(ccsDir, 'config.yaml');
        const cacheDir = path.join(ccsDir, 'cache');
        if (fs.existsSync(configYaml))
            fs.unlinkSync(configYaml);
        // Restore cache files to original locations
        if (fs.existsSync(cacheDir)) {
            moveIfExists(path.join(cacheDir, 'usage.json'), path.join(ccsDir, 'usage-cache.json'));
            moveIfExists(path.join(cacheDir, 'update-check.json'), path.join(ccsDir, 'update-check.json'));
            // Remove cache dir if empty
            const remaining = fs.readdirSync(cacheDir);
            if (remaining.length === 0) {
                fs.rmdirSync(cacheDir);
            }
        }
        // Restore files from backup
        const files = fs.readdirSync(managedBackupPath);
        for (const file of files) {
            fs.copyFileSync(path.join(managedBackupPath, file), path.join(ccsDir, file));
        }
        return true;
    }
    catch (err) {
        console.error(`[X] Rollback failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
        return false;
    }
}
exports.rollback = rollback;
// --- Helper Functions ---
/**
 * Read JSON file safely, returning null on error or if file doesn't exist.
 */
function readJsonSafe(filePath) {
    if (!fs.existsSync(filePath))
        return null;
    try {
        return JSON.parse(fs.readFileSync(filePath, 'utf8'));
    }
    catch {
        return null;
    }
}
/**
 * Move file if it exists. Returns true if moved, false if source didn't exist.
 */
function moveIfExists(from, to) {
    if (fs.existsSync(from)) {
        fs.renameSync(from, to);
        return true;
    }
    return false;
}
/**
 * Create backup of all v1 config files.
 */
async function createBackup(srcDir, backupDir) {
    // Check if backup already exists (prevent overwriting)
    if (fs.existsSync(backupDir)) {
        // Add timestamp suffix to make unique
        const suffix = Date.now().toString(36);
        const uniqueBackupDir = `${backupDir}-${suffix}`;
        fs.mkdirSync(uniqueBackupDir, { recursive: true });
        await performBackup(srcDir, uniqueBackupDir);
        return;
    }
    fs.mkdirSync(backupDir, { recursive: true });
    await performBackup(srcDir, backupDir);
}
async function performBackup(srcDir, backupDir) {
    const filesToBackup = ['config.json', 'profiles.json', 'usage-cache.json', 'update-check.json'];
    // Also backup *.settings.json files
    const allFiles = fs.readdirSync(srcDir);
    const settingsFiles = allFiles.filter((f) => f.endsWith('.settings.json'));
    filesToBackup.push(...settingsFiles);
    for (const file of filesToBackup) {
        const src = path.join(srcDir, file);
        if (fs.existsSync(src)) {
            fs.copyFileSync(src, path.join(backupDir, file));
        }
    }
}
/**
 * Auto-migrate on first run after update.
 * Silent if already migrated or no config exists.
 * Shows friendly message with backup location on success.
 *
 * Also handles inline profile migration when config.yaml exists but is missing
 * profiles from config.json (Fix for issue #195).
 */
async function autoMigrate() {
    // Skip in test environment
    if (process.env.NODE_ENV === 'test' || process.env.CCS_SKIP_MIGRATION === '1') {
        return;
    }
    // Check if full migration is needed (no config.yaml exists)
    if (needsMigration()) {
        const result = await migrate(false);
        if (result.success) {
            console.log('');
            console.log((0, ui_1.infoBox)('Migrated to unified config (config.yaml)', 'SUCCESS'));
            console.log(`  Backup: ${result.backupPath}`);
            console.log(`  Items:  ${result.migratedFiles.length} migrated`);
            if (result.warnings.length > 0) {
                for (const warning of result.warnings) {
                    console.log((0, ui_1.warn)(warning));
                }
            }
            console.log(`  Rollback: ccs migrate --rollback ${result.backupPath}`);
            console.log('');
        }
        else {
            console.log('');
            console.log((0, ui_1.infoBox)('Migration failed - using legacy config', 'WARNING'));
            console.log(`  Error: ${result.error}`);
            console.log('  Retry: ccs migrate');
            console.log('');
        }
        return;
    }
    // Check if inline profile migration is needed (config.yaml exists but missing profiles)
    // CRIT-2: Load data once and pass to migrateProfilesToUnified to avoid TOCTOU race
    const migrationData = loadMigrationCheckData();
    if (migrationData.needsMigration) {
        const result = await migrateProfilesToUnified(migrationData);
        if (result.success && result.migratedFiles.length > 0) {
            console.log('');
            console.log((0, ui_1.infoBox)('Migrated legacy profiles to config.yaml', 'SUCCESS'));
            console.log(`  Profiles: ${result.migratedFiles.join(', ')}`);
            // H7: Show collision warnings if any
            if (result.warnings.length > 0) {
                for (const warning of result.warnings) {
                    console.log((0, ui_1.warn)(warning));
                }
            }
            console.log('');
        }
    }
}
exports.autoMigrate = autoMigrate;
/**
 * Migrate only profiles from config.json to existing config.yaml.
 * Used when config.yaml exists but is missing profiles.
 *
 * @param preloadedData - Optional pre-loaded config data from loadMigrationCheckData()
 *                        for TOCTOU-safe operations. If not provided, reads configs fresh.
 */
async function migrateProfilesToUnified(preloadedData) {
    const ccsDir = (0, config_manager_1.getCcsDir)();
    const migratedFiles = [];
    const warnings = [];
    try {
        // Use preloaded data if available (CRIT-2: TOCTOU fix)
        const oldConfig = preloadedData?.legacyConfig ?? readJsonSafe(path.join(ccsDir, 'config.json'));
        const unifiedConfig = preloadedData?.unifiedConfig ?? (0, unified_config_loader_1.loadUnifiedConfig)();
        if (!oldConfig?.profiles || !unifiedConfig) {
            return { success: true, migratedFiles, warnings };
        }
        let modified = false;
        // Migrate API profiles from config.json
        for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
            const sourceName = name.trim();
            const targetName = (0, profile_compat_1.resolveAliasToCanonical)(sourceName);
            const pathStr = settingsPath;
            const canonicalEntryValue = oldConfig.profiles[targetName];
            const canonicalPathFromLegacyConfig = sourceName !== targetName && typeof canonicalEntryValue === 'string'
                ? canonicalEntryValue
                : undefined;
            // Deterministic priority: explicit canonical profile wins over legacy alias rename.
            if (canonicalPathFromLegacyConfig !== undefined) {
                if (canonicalPathFromLegacyConfig !== pathStr) {
                    warnings.push(`Skipped ${sourceName}: canonical profile "${targetName}" exists in config.json with different settings (${canonicalPathFromLegacyConfig})`);
                }
                continue;
            }
            // H7: Detect collision - profile exists in both configs
            if (unifiedConfig.profiles[targetName]) {
                // Check if settings differ (potential data loss)
                const existingSettings = unifiedConfig.profiles[targetName].settings;
                if (existingSettings && existingSettings !== pathStr) {
                    warnings.push(`Profile "${targetName}" exists in both configs with different settings - keeping existing (${existingSettings}), skipping legacy ${sourceName} (${pathStr})`);
                }
                continue;
            }
            const expandedPath = (0, helpers_1.expandPath)(pathStr);
            // Verify settings file exists
            if (!fs.existsSync(expandedPath)) {
                warnings.push(`Skipped ${sourceName}: settings file not found at ${pathStr}`);
                continue;
            }
            // Store reference to settings file
            unifiedConfig.profiles[targetName] = {
                type: 'api',
                settings: pathStr,
            };
            migratedFiles.push(targetName);
            if (targetName !== sourceName) {
                warnings.push(`Renamed legacy API profile "${sourceName}" to "${targetName}" (ccs kimi API profile is now ccs km)`);
            }
            modified = true;
        }
        // Save if modified
        if (modified) {
            (0, unified_config_loader_1.saveUnifiedConfig)(unifiedConfig);
        }
        return { success: true, migratedFiles, warnings };
    }
    catch (err) {
        return {
            success: false,
            error: err instanceof Error ? err.message : 'Unknown error',
            migratedFiles,
            warnings,
        };
    }
}
//# sourceMappingURL=migration-manager.js.map