/**
 * Increment Archiver - Smart archiving system for completed increments
 *
 * Keeps the increments folder clean by archiving old/completed increments
 * while preserving active work and maintaining full history.
 */
import * as fs from '../../utils/fs-native.js';
import * as path from 'path';
import { glob } from 'glob';
import { ConfigManager } from '../config-manager.js';
import { detectDuplicatesByNumber } from './duplicate-detector.js';
import { consoleLogger } from '../../utils/logger.js';
// Archiver-specific logger with emoji formatting
class ArchiverLogger {
    constructor(baseLogger) {
        this.baseLogger = baseLogger;
    }
    info(message) { this.baseLogger.log(`ℹ️  ${message}`); }
    success(message) { this.baseLogger.log(`✅ ${message}`); }
    warn(message) { this.baseLogger.warn(`⚠️  ${message}`); }
    error(message, error) { this.baseLogger.error(`❌ ${message}`, error); }
    debug(message) { if (process.env.DEBUG)
        this.baseLogger.log(`🔍 ${message}`); }
    // Logger interface implementation
    log(message) { this.baseLogger.log(message); }
}
export class IncrementArchiver {
    constructor(rootDir, options = {}) {
        this.rootDir = rootDir;
        this.logger = new ArchiverLogger(options.logger ?? consoleLogger);
        this.config = new ConfigManager(rootDir);
        this.incrementsDir = path.join(rootDir, '.specweave', 'increments');
        this.archiveDir = path.join(this.incrementsDir, '_archive');
        this.abandonedDir = path.join(this.incrementsDir, '_abandoned');
    }
    /**
     * Archive increments based on options
     *
     * CRITICAL FIX (2025-11-26): --keep-last N now means "exactly N increments remain"
     *
     * Previous bug: External sync protection would ADD exceptions on top of keepLast,
     * resulting in more than N increments remaining. Now we use a "greedy archive"
     * algorithm that archives from oldest until exactly N remain.
     *
     * Algorithm:
     * 1. Sort all increments by number (oldest first)
     * 2. Calculate how many need to remain: keepLast (or apply other filters)
     * 3. Walk from oldest, archiving each eligible increment
     * 4. Protected increments (external sync, active) count toward the "remaining" quota
     * 5. Stop when exactly keepLast increments remain
     */
    async archive(options = {}) {
        const result = {
            archived: [],
            skipped: [],
            errors: [],
            totalSize: 0
        };
        // Ensure archive directory exists
        if (!options.dryRun) {
            await fs.ensureDir(this.archiveDir);
        }
        // Get all increments sorted by number (oldest first)
        const allIncrements = await this.getIncrements();
        // Handle specific increments mode (explicit list)
        if (options.increments && options.increments.length > 0) {
            return this.archiveSpecificIncrements(options.increments, options, result);
        }
        // STRICT MODE: --last N ignores ALL protections, keeps exactly N newest
        if (options.strictLast !== undefined) {
            return this.archiveStrictLast(allIncrements, options.strictLast, options, result);
        }
        // Handle pattern mode
        if (options.pattern) {
            return this.archiveByPattern(allIncrements, options, result);
        }
        // Handle olderThanDays mode
        if (options.olderThanDays !== undefined) {
            return this.archiveByAge(allIncrements, options, result);
        }
        // Handle keepLast mode (default behavior)
        // CRITICAL SAFETY: Default to 5 if no criteria provided
        const effectiveKeepLast = options.keepLast ?? 5;
        if (options.keepLast === undefined) {
            this.logger.warn(`No filtering criteria provided - defaulting to --keep-last 5 for safety`);
            this.logger.info(`Use --keep-last N to explicitly set how many increments to keep`);
        }
        return this.archiveWithKeepLast(allIncrements, effectiveKeepLast, options, result);
    }
    /**
     * Archive specific increments by ID
     */
    async archiveSpecificIncrements(incrementIds, options, result) {
        const allIncrements = await this.getIncrements();
        for (const target of incrementIds) {
            const increment = allIncrements.find(inc => {
                const incNumber = inc.split('-')[0];
                return inc === target || incNumber === target.padStart(4, '0');
            });
            if (!increment) {
                this.logger.warn(`Increment not found: ${target}`);
                result.errors.push(target);
                continue;
            }
            await this.tryArchiveIncrement(increment, options, result);
        }
        if (!options.dryRun) {
            result.totalSize = await this.calculateSize(result.archived);
        }
        return result;
    }
    /**
     * Archive increments matching a pattern
     */
    async archiveByPattern(allIncrements, options, result) {
        const regex = new RegExp(options.pattern, 'i');
        const matching = allIncrements.filter(inc => regex.test(inc));
        for (const increment of matching) {
            await this.tryArchiveIncrement(increment, options, result);
        }
        if (!options.dryRun) {
            result.totalSize = await this.calculateSize(result.archived);
        }
        return result;
    }
    /**
     * Archive increments older than N days
     */
    async archiveByAge(allIncrements, options, result) {
        const cutoffDate = new Date();
        cutoffDate.setDate(cutoffDate.getDate() - options.olderThanDays);
        for (const increment of allIncrements) {
            const metadata = await this.getMetadata(increment);
            let isOldEnough = false;
            if (metadata?.lastActivity) {
                const lastActivity = new Date(metadata.lastActivity);
                isOldEnough = lastActivity < cutoffDate;
            }
            else {
                const incPath = path.join(this.incrementsDir, increment);
                const stats = await fs.stat(incPath);
                isOldEnough = stats.mtime < cutoffDate;
            }
            if (isOldEnough) {
                await this.tryArchiveIncrement(increment, options, result);
            }
        }
        if (!options.dryRun) {
            result.totalSize = await this.calculateSize(result.archived);
        }
        return result;
    }
    /**
     * Archive with --keep-last N semantics
     *
     * v0.30.0 CRITICAL FIX: "keep last 5" means "keep the 5 HIGHEST-NUMBERED increments"
     * v0.33.0 CRITICAL FIX: External increments (E suffix) are SEPARATE from internal!
     *
     * Previous bug: Counted only active increments. If 5 active with gaps (0072, 0085-0088),
     * nothing would archive because 5 <= 5.
     *
     * v0.33.0 bug fix: parseInt("0118E") → 118, losing E suffix! This caused 0118E
     * to be treated as same number as 0118, potentially archiving the wrong one.
     *
     * NEW Algorithm:
     * 1. Get ALL increment KEYS (number + E suffix) to find the global "last N"
     * 2. Extract key using extractIncrementKey() which preserves E suffix
     * 3. Calculate which keys should remain: the N highest by number (E variants separate)
     * 4. Archive any active increment NOT in that list
     * 5. Protected increments (external sync) are SKIPPED but logged as needing attention
     */
    async archiveWithKeepLast(allIncrements, keepLast, options, result) {
        // Get ALL increment KEYS (preserving E suffix!) to determine global "last N"
        // Key format: "0118" or "0118E" - preserves external flag
        const archivedIncrements = await this.listArchived();
        const allIncrementKeys = [
            ...allIncrements.map(inc => this.extractIncrementKey(inc)),
            ...archivedIncrements.map(inc => this.extractIncrementKey(inc))
        ].sort((a, b) => {
            // Sort by number descending (highest first)
            // External (E) comes AFTER internal with same number: 0118 before 0118E
            const numA = parseInt(a.replace('E', ''));
            const numB = parseInt(b.replace('E', ''));
            if (numA !== numB)
                return numB - numA;
            // Same number: internal before external (so 0118 sorts before 0118E)
            return a.endsWith('E') ? 1 : -1;
        });
        // Get unique keys and find the N highest
        const uniqueKeys = [...new Set(allIncrementKeys)];
        const keepKeys = new Set(uniqueKeys.slice(0, keepLast));
        this.logger.info(`Keep last ${keepLast} by NUMBER: ${[...keepKeys].sort((a, b) => {
            const numA = parseInt(a.replace('E', ''));
            const numB = parseInt(b.replace('E', ''));
            return numA - numB;
        }).join(', ')}`);
        // Find active increments NOT in the "keep" list
        const toArchive = [];
        const toKeep = [];
        for (const increment of allIncrements) {
            const incKey = this.extractIncrementKey(increment);
            if (keepKeys.has(incKey)) {
                toKeep.push(increment);
            }
            else {
                toArchive.push(increment);
            }
        }
        if (toArchive.length === 0) {
            this.logger.info(`All ${allIncrements.length} active increments are in the last ${keepLast} by number`);
            return result;
        }
        this.logger.info(`Will archive ${toArchive.length} increments not in last ${keepLast}: ${toArchive.map(i => this.extractIncrementKey(i)).join(', ')}`);
        // Archive each increment not in the keep list
        for (const increment of toArchive) {
            const archiveAttempt = await this.tryArchiveIncrement(increment, options, result);
            if (!archiveAttempt.archived && !archiveAttempt.error) {
                // Increment was protected - warn that it needs attention
                this.logger.warn(`⚠️  ${increment} should be archived (not in last ${keepLast}) but is protected`);
                this.logger.warn(`   → Close external sync (GitHub/JIRA/ADO) to allow archiving`);
            }
        }
        if (!options.dryRun) {
            result.totalSize = await this.calculateSize(result.archived);
        }
        return result;
    }
    /**
     * Extract increment key from increment name
     *
     * Preserves the E suffix for external increments!
     * - "0118-my-feature" → "0118"
     * - "0118E-external-issue" → "0118E"
     *
     * CRITICAL (v0.33.0): This is NOT the same as parseInt()!
     * parseInt("0118E") → 118 (WRONG - loses E)
     * extractIncrementKey("0118E-name") → "0118E" (CORRECT)
     */
    extractIncrementKey(increment) {
        const match = increment.match(/^(\d+)(E)?/);
        if (!match) {
            return increment.split('-')[0]; // Fallback
        }
        return match[2] ? `${match[1]}E` : match[1];
    }
    /**
     * STRICT archive: Keep exactly last N increments, ignoring ALL protections
     *
     * This mode:
     * - Ignores external sync status (GitHub issues, JIRA tickets, ADO work items)
     * - Ignores active/paused status
     * - Ignores uncommitted changes
     * - Archives EVERYTHING except the last N by increment number
     *
     * Use when you want a clean folder with exactly N increments, no exceptions.
     */
    async archiveStrictLast(allIncrements, lastN, options, result) {
        const total = allIncrements.length;
        if (total <= lastN) {
            this.logger.info(`Only ${total} increments exist, nothing to archive (keeping ${lastN})`);
            return result;
        }
        // Calculate exactly which increments to keep (the last N by number)
        const toKeep = allIncrements.slice(-lastN);
        const toArchive = allIncrements.slice(0, -lastN);
        this.logger.info(`STRICT MODE: Archiving ${toArchive.length} increments, keeping last ${lastN}`);
        this.logger.warn(`⚠️  This ignores ALL protections (external sync, active status, etc.)`);
        this.logger.info(`Keeping: ${toKeep.map(i => i.split('-')[0]).join(', ')}`);
        // Archive all increments that are not in the keep list
        for (const increment of toArchive) {
            try {
                // Check if already archived (the only check we do)
                const archivePath = path.join(this.archiveDir, increment);
                if (await fs.pathExists(archivePath)) {
                    this.logger.debug(`${increment} already in archive, skipping`);
                    result.skipped.push(increment);
                    continue;
                }
                if (options.dryRun) {
                    this.logger.info(`[DRY RUN] Would archive: ${increment}`);
                    result.archived.push(increment);
                }
                else {
                    await this.archiveIncrement(increment);
                    result.archived.push(increment);
                    this.logger.success(`Archived: ${increment}`);
                }
            }
            catch (error) {
                result.errors.push(increment);
                this.logger.error(`Failed to archive ${increment}: ${error}`);
            }
        }
        if (!options.dryRun) {
            result.totalSize = await this.calculateSize(result.archived);
        }
        return result;
    }
    /**
     * Try to archive a single increment, handling all checks
     * Returns { archived: boolean, error: boolean }
     */
    async tryArchiveIncrement(increment, options, result) {
        try {
            const canArchive = await this.shouldArchive(increment, options);
            if (canArchive) {
                if (options.dryRun) {
                    this.logger.info(`[DRY RUN] Would archive: ${increment}`);
                    result.archived.push(increment);
                }
                else {
                    await this.archiveIncrement(increment);
                    result.archived.push(increment);
                    this.logger.success(`Archived: ${increment}`);
                }
                return { archived: true, error: false };
            }
            else {
                result.skipped.push(increment);
                return { archived: false, error: false };
            }
        }
        catch (error) {
            result.errors.push(increment);
            this.logger.error(`Failed to archive ${increment}: ${error}`);
            return { archived: false, error: true };
        }
    }
    /**
     * Get all increment directories (excluding _archive and _abandoned)
     */
    async getIncrements() {
        const pattern = path.join(this.incrementsDir, '[0-9]*-*');
        const allPaths = await glob(pattern);
        // Filter to only directories
        const increments = [];
        for (const p of allPaths) {
            const stats = await fs.stat(p);
            if (stats.isDirectory()) {
                increments.push(p);
            }
        }
        return increments
            .map(p => path.basename(p))
            .sort((a, b) => {
            const numA = parseInt(a.split('-')[0]);
            const numB = parseInt(b.split('-')[0]);
            return numA - numB;
        });
    }
    /**
     * Determine if an increment should be archived
     */
    async shouldArchive(increment, options) {
        // Check if already in archive
        const archivePath = path.join(this.archiveDir, increment);
        if (await fs.pathExists(archivePath)) {
            this.logger.debug(`${increment} already archived`);
            return false;
        }
        // Get metadata
        const metadata = await this.getMetadata(increment);
        // Preserve active/paused increments by default
        if (options.preserveActive !== false && metadata?.status) {
            if (['active', 'paused'].includes(metadata.status)) {
                this.logger.debug(`${increment} is ${metadata.status}, preserving`);
                return false;
            }
        }
        // Check for active external sync (GitHub, JIRA, ADO) - MUST check before archiveCompleted
        if (metadata) {
            const hasActiveSync = (metadata.github && !metadata.github.closed) ||
                (metadata.jira && metadata.jira.status !== 'Done') ||
                (metadata.ado && metadata.ado.state !== 'Closed');
            if (hasActiveSync) {
                this.logger.warn(`${increment} has active external sync, skipping`);
                return false;
            }
        }
        // Archive completed increments if requested
        if (options.archiveCompleted && metadata?.status === 'completed') {
            return true;
        }
        // Check for uncommitted changes
        const hasUncommittedChanges = await this.hasUncommittedChanges(increment);
        if (hasUncommittedChanges) {
            this.logger.warn(`${increment} has uncommitted changes, skipping`);
            return false;
        }
        return true;
    }
    /**
     * Archive a single increment
     *
     * CRITICAL FIX (2025-12-07): Sync to living docs BEFORE moving to archive!
     *
     * Previous bug: Living docs were only reorganized (moved) after archive, but
     * content was never synced. If feature folder didn't exist, nothing got archived.
     *
     * Flow now:
     * 1. SYNC: Create/update living docs from spec.md (while increment is active)
     * 2. MOVE: Move increment to _archive/
     * 3. ARCHIVE: Move feature folder to _archive/ (if all increments archived)
     */
    async archiveIncrement(increment) {
        const sourcePath = path.join(this.incrementsDir, increment);
        const targetPath = path.join(this.archiveDir, increment);
        // CRITICAL: Check for duplicates before archiving
        const numberMatch = increment.match(/^(\d+)/);
        if (numberMatch) {
            const incrementNumber = numberMatch[1];
            const duplicates = await detectDuplicatesByNumber(incrementNumber, this.rootDir);
            // Check if any duplicates exist in archive or abandoned
            const archiveDuplicates = duplicates.filter(d => d.path.includes('_archive') || d.path.includes('_abandoned'));
            if (archiveDuplicates.length > 0) {
                const locations = archiveDuplicates.map(d => d.path).join('\n  - ');
                throw new Error(`Cannot archive ${increment}: Increment number ${incrementNumber} already exists in:\n  - ${locations}\n\n` +
                    `Resolution options:\n` +
                    `  1. Delete the existing archive/abandoned version first\n` +
                    `  2. Use --force to overwrite (not recommended)\n` +
                    `  3. Run sw:fix-duplicates to resolve conflicts`);
            }
        }
        // STEP 1: Sync to living docs BEFORE moving (increment must be in active folder!)
        // This ensures feature folder exists with latest content before archiving
        await this.syncToLivingDocsBeforeArchive(increment);
        // STEP 2: Move the directory to archive
        await fs.move(sourcePath, targetPath, { overwrite: false });
        // STEP 3: Archive feature folder (now that increment is archived)
        await this.updateReferences(increment);
    }
    /**
     * Sync increment to living docs BEFORE archiving
     *
     * CRITICAL: This must happen while the increment is still in the active folder!
     * LivingDocsSync.syncIncrement() checks for active folder location and skips
     * if increment is already archived.
     *
     * This ensures:
     * - Feature folder exists in living docs
     * - FEATURE.md has latest content
     * - User story files are created/updated
     * - Tasks are synced to living docs
     */
    async syncToLivingDocsBeforeArchive(increment) {
        try {
            // CRITICAL: Skip living docs sync in non-interactive environments (tests, CI)
            // LivingDocsSync uses @inquirer/prompts which hangs forever without TTY
            const isNonInteractive = !process.stdout.isTTY || process.env.CI || process.env.VITEST;
            if (isNonInteractive) {
                this.logger.debug(`Skipping living docs sync for ${increment} (non-interactive environment)`);
                return;
            }
            // Check if spec.md exists (required for sync)
            const specPath = path.join(this.incrementsDir, increment, 'spec.md');
            if (!await fs.pathExists(specPath)) {
                this.logger.debug(`No spec.md for ${increment}, skipping living docs sync`);
                return;
            }
            this.logger.info(`📝 Syncing ${increment} to living docs before archive...`);
            const { LivingDocsSync } = await import('../living-docs/living-docs-sync.js');
            const sync = new LivingDocsSync(this.rootDir);
            const result = await sync.syncIncrement(increment, { force: true });
            if (result.success) {
                if (result.filesCreated.length > 0) {
                    this.logger.success(`   ✅ Created/updated ${result.filesCreated.length} files in ${result.featureId}`);
                }
                else {
                    this.logger.info(`   ℹ️  ${result.featureId} already up to date`);
                }
            }
            else if (result.errors.length > 0) {
                // Log errors but don't fail archiving
                this.logger.warn(`   ⚠️  Sync warnings: ${result.errors.join(', ')}`);
            }
        }
        catch (error) {
            // Log error but don't fail archiving - living docs sync is best-effort
            this.logger.warn(`   ⚠️  Could not sync to living docs: ${error}`);
            this.logger.info(`   ℹ️  You can manually sync later with: sw:sync-docs ${increment}`);
        }
    }
    /**
     * Get metadata for an increment
     */
    async getMetadata(increment) {
        const metadataPath = path.join(this.incrementsDir, increment, 'metadata.json');
        if (await fs.pathExists(metadataPath)) {
            try {
                return await fs.readJson(metadataPath);
            }
            catch (error) {
                this.logger.debug(`Failed to read metadata for ${increment}: ${error}`);
            }
        }
        return null;
    }
    /**
     * Check if increment has uncommitted changes
     */
    async hasUncommittedChanges(increment) {
        // This would integrate with git to check for uncommitted changes
        // For now, return false to allow archiving
        return false;
    }
    /**
     * Update references after archiving
     */
    async updateReferences(increment) {
        // Import and run feature archiving to maintain consistency
        try {
            this.logger.info(`🔄 Reorganizing living docs after archiving ${increment}...`);
            const { FeatureArchiver } = await import('../living-docs/feature-archiver.js');
            const featureArchiver = new FeatureArchiver(this.rootDir);
            // Archive features that have all their increments archived
            // CRITICAL: Use forceArchiveWhenAllIncrementsArchived to ensure comprehensive reorganization
            const result = await featureArchiver.archiveFeatures({
                dryRun: false,
                updateLinks: true,
                preserveActiveFeatures: true,
                archiveOrphanedFeatures: false,
                archiveOrphanedEpics: false,
                forceArchiveWhenAllIncrementsArchived: true // Override active projects check when all increments archived
            });
            if (result.archivedFeatures.length > 0) {
                this.logger.success(`✅ Archived ${result.archivedFeatures.length} features: ${result.archivedFeatures.join(', ')}`);
                this.logger.info(`   Features moved to {project}/_archive/ folders`);
            }
            else {
                this.logger.info(`ℹ️  No features to archive (all active or already archived)`);
            }
            if (result.archivedEpics.length > 0) {
                this.logger.success(`✅ Archived ${result.archivedEpics.length} epics: ${result.archivedEpics.join(', ')}`);
            }
            if (result.updatedLinks.length > 0) {
                const filesUpdated = new Set(result.updatedLinks.map(u => u.file)).size;
                this.logger.success(`✅ Updated ${result.updatedLinks.length} links in ${filesUpdated} files`);
            }
            if (result.errors.length > 0) {
                this.logger.warn(`⚠️  Encountered ${result.errors.length} errors during reorganization:`);
                result.errors.forEach(err => this.logger.warn(`   - ${err}`));
            }
            this.logger.success(`✅ Living docs reorganization complete`);
        }
        catch (error) {
            this.logger.warn(`Could not update feature archives: ${error}`);
        }
    }
    /**
     * Update references after restoring (mirror of archiving behavior)
     *
     * When an increment is restored from archive, check if its linked feature
     * is also in the archive. If so, restore the feature to maintain consistency
     * between increments and living docs.
     */
    async updateReferencesOnRestore(increment) {
        try {
            this.logger.info(`🔄 Checking living docs sync after restoring ${increment}...`);
            // 1. Parse spec.md to get feature_id (explicit or inferred)
            const specPath = path.join(this.incrementsDir, increment, 'spec.md');
            if (!await fs.pathExists(specPath)) {
                this.logger.debug(`No spec.md found for ${increment}, skipping living docs sync`);
                return;
            }
            const content = await fs.readFile(specPath, 'utf-8');
            // Get feature ID (explicit or auto-inferred from increment number)
            const featureId = await this.getFeatureIdForIncrement(increment, content);
            if (!featureId) {
                this.logger.debug(`No feature linkage for ${increment}, skipping living docs sync`);
                return;
            }
            this.logger.info(`   Feature linkage: ${featureId}`);
            // 2. Check if feature is in archive (v5.0.0: check project folders, not _features)
            // Get the default/first project to check
            const specsDir = path.join(this.rootDir, '.specweave', 'docs', 'internal', 'specs');
            const projectFolders = await fs.readdir(specsDir, { withFileTypes: true });
            const projects = projectFolders
                .filter(d => d.isDirectory() && !d.name.startsWith('_'))
                .map(d => d.name);
            const defaultProject = projects[0] || 'default';
            const archivePath = path.join(specsDir, defaultProject, '_archive', featureId);
            const activePath = path.join(specsDir, defaultProject, featureId);
            const featureInArchive = await fs.pathExists(archivePath);
            const featureAlreadyActive = await fs.pathExists(activePath);
            if (!featureInArchive) {
                if (featureAlreadyActive) {
                    this.logger.info(`   ✅ Feature ${featureId} already in active location (no action needed)`);
                    return;
                }
                else {
                    // Feature doesn't exist - CREATE it by syncing from increment
                    this.logger.info(`   📝 Feature ${featureId} doesn't exist - creating from increment spec...`);
                    try {
                        const { LivingDocsSync } = await import('../living-docs/living-docs-sync.js');
                        const sync = new LivingDocsSync(this.rootDir);
                        const result = await sync.syncIncrement(increment);
                        if (result.success) {
                            this.logger.success(`   ✅ Created feature ${featureId} with ${result.filesCreated.length} files`);
                        }
                        else {
                            this.logger.warn(`   ⚠️  Failed to create feature: ${result.errors.join(', ')}`);
                        }
                    }
                    catch (syncError) {
                        this.logger.warn(`   ⚠️  Could not create feature: ${syncError}`);
                    }
                    return;
                }
            }
            if (featureAlreadyActive) {
                // Duplicate detected - archive has feature but active also has it
                // This can happen if living docs sync created the folder after a previous restore
                this.logger.warn(`   ⚠️  Feature ${featureId} exists in BOTH archive and active locations`);
                this.logger.info(`   Running duplicate cleanup...`);
                const { FeatureArchiver } = await import('../living-docs/feature-archiver.js');
                const featureArchiver = new FeatureArchiver(this.rootDir);
                const cleanupResult = await featureArchiver.cleanupDuplicates();
                if (cleanupResult.cleaned.length > 0) {
                    this.logger.success(`   ✅ Cleaned ${cleanupResult.cleaned.length} duplicate folders`);
                }
                return;
            }
            // 3. Restore feature from archive
            this.logger.info(`   📦 Restoring feature ${featureId} from archive...`);
            const { FeatureArchiver } = await import('../living-docs/feature-archiver.js');
            const featureArchiver = new FeatureArchiver(this.rootDir);
            await featureArchiver.restoreFeature(featureId);
            this.logger.success(`✅ Restored feature ${featureId} from archive (linked to ${increment})`);
            this.logger.info(`   Feature moved: {project}/_archive/${featureId}/ → {project}/${featureId}/`);
            this.logger.info(`   Links updated throughout codebase`);
        }
        catch (error) {
            this.logger.warn(`Could not sync living docs on restore: ${error}`);
            this.logger.info(`You may need to manually restore the feature with: sw:restore-feature <feature-id>`);
        }
    }
    /**
     * Calculate total size of archived increments
     */
    async calculateSize(increments) {
        let totalSize = 0;
        for (const increment of increments) {
            const incPath = path.join(this.archiveDir, increment);
            if (await fs.pathExists(incPath)) {
                totalSize += await this.getDirectorySize(incPath);
            }
        }
        return totalSize;
    }
    /**
     * Get directory size recursively
     */
    async getDirectorySize(dirPath) {
        let size = 0;
        const files = await glob(path.join(dirPath, '**/*'));
        for (const file of files) {
            const stats = await fs.stat(file);
            if (stats.isFile()) {
                size += stats.size;
            }
        }
        return size;
    }
    /**
     * Restore an increment from archive
     */
    async restore(increment) {
        const sourcePath = path.join(this.archiveDir, increment);
        const targetPath = path.join(this.incrementsDir, increment);
        if (!await fs.pathExists(sourcePath)) {
            throw new Error(`Increment ${increment} not found in archive`);
        }
        if (await fs.pathExists(targetPath)) {
            throw new Error(`Increment ${increment} already exists in main folder`);
        }
        // CRITICAL: Check for duplicates before restoring
        const numberMatch = increment.match(/^(\d+)/);
        if (numberMatch) {
            const incrementNumber = numberMatch[1];
            const duplicates = await detectDuplicatesByNumber(incrementNumber, this.rootDir);
            // Check if any duplicates exist in active increments
            const activeDuplicates = duplicates.filter(d => !d.path.includes('_archive') && !d.path.includes('_abandoned'));
            if (activeDuplicates.length > 0) {
                const locations = activeDuplicates.map(d => d.path).join('\n  - ');
                throw new Error(`Cannot restore ${increment}: Increment number ${incrementNumber} already exists in active folder:\n  - ${locations}\n\n` +
                    `Resolution options:\n` +
                    `  1. Delete/archive the existing active version first\n` +
                    `  2. Run sw:fix-duplicates to resolve conflicts`);
            }
        }
        await fs.move(sourcePath, targetPath);
        this.logger.success(`Restored ${increment} from archive`);
        // Sync living docs on restore (mirror of archiving behavior)
        await this.updateReferencesOnRestore(increment);
    }
    /**
     * List archived increments
     */
    async listArchived() {
        if (!await fs.pathExists(this.archiveDir)) {
            return [];
        }
        const pattern = path.join(this.archiveDir, '[0-9]*-*');
        const allPaths = await glob(pattern);
        // Filter to only directories
        const archived = [];
        for (const p of allPaths) {
            const stats = await fs.stat(p);
            if (stats.isDirectory()) {
                archived.push(p);
            }
        }
        return archived
            .map(p => path.basename(p))
            .sort((a, b) => {
            const numA = parseInt(a.split('-')[0]);
            const numB = parseInt(b.split('-')[0]);
            return numA - numB;
        });
    }
    /**
     * Get feature ID for an increment (explicit or inferred)
     *
     * Priority:
     * 1. Explicit linkage (feature_id or epic in frontmatter)
     * 2. Auto-inferred from increment number (0041 → FS-041)
     */
    async getFeatureIdForIncrement(increment, specContent) {
        // 1. Try explicit linkage first
        const featureIdMatch = specContent.match(/^feature_id:\s*["']?([^"'\n]+)["']?$/m);
        const epicMatch = specContent.match(/^epic:\s*["']?([^"'\n]+)["']?$/m);
        const explicitFeatureId = featureIdMatch ? featureIdMatch[1].trim() :
            epicMatch ? epicMatch[1].trim() : null;
        if (explicitFeatureId) {
            this.logger.debug(`Found explicit feature linkage: ${explicitFeatureId}`);
            return explicitFeatureId;
        }
        // 2. Auto-infer from increment number
        const inferredFeatureId = this.inferFeatureIdFromIncrement(increment);
        if (inferredFeatureId) {
            this.logger.info(`   Auto-inferred feature ID from increment: ${increment} → ${inferredFeatureId}`);
            return inferredFeatureId;
        }
        // 3. No linkage possible
        return null;
    }
    /**
     * Infer feature ID from increment number
     *
     * Examples:
     * - "0041-living-docs-test-fixes" → "FS-041"
     * - "0123-my-feature" → "FS-123"
     * - "0111E-external-issue" → "FS-111E" (external increment → external feature)
     * - "temp-experiment" → null (no number)
     *
     * CRITICAL (v0.33.0): External increments (with E suffix like 0111E-...)
     * MUST map to external features (FS-111E), not internal ones (FS-111).
     */
    inferFeatureIdFromIncrement(increment) {
        // Extract 4-digit number prefix and optional E suffix
        // Pattern: NNNN or NNNNE (e.g., 0041, 0111E)
        const match = increment.match(/^(\d{4})(E)?/);
        if (!match) {
            return null; // Can't infer (no number prefix)
        }
        const number = parseInt(match[1], 10);
        const isExternal = match[2] === 'E';
        // Convert to feature ID format: 41 → "FS-041", 111E → "FS-111E"
        const featureId = `FS-${number.toString().padStart(3, '0')}`;
        return isExternal ? `${featureId}E` : featureId;
    }
    /**
     * Get archive statistics
     */
    async getStats() {
        const active = await this.getIncrements();
        const archived = await this.listArchived();
        // Count abandoned
        let abandoned = 0;
        if (await fs.pathExists(this.abandonedDir)) {
            const abandonedDirs = await glob(path.join(this.abandonedDir, '[0-9]*-*'));
            abandoned = abandonedDirs.length;
        }
        // Calculate total archive size
        const totalSize = await this.calculateSize(archived);
        return {
            active: active.length,
            archived: archived.length,
            abandoned,
            totalSize,
            oldestActive: active[0] || null,
            newestArchived: archived[archived.length - 1] || null
        };
    }
}
//# sourceMappingURL=increment-archiver.js.map