/**
 * Undo/Checkpoint System
 *
 * Features:
 * - File state checkpoints
 * - Undo/redo operations
 * - Checkpoint naming and tagging
 * - Diff viewing between checkpoints
 * - Automatic checkpoints before dangerous operations
 * - Git integration for version control
 *
 * Allows reverting changes made by Grok CLI.
 */
import fs from 'fs-extra';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';
import { spawn } from 'child_process';
import { diff_match_patch } from 'diff-match-patch';
import { getErrorMessage } from '../types/index.js';
import { TypedEventEmitter } from '../events/index.js';
const DEFAULT_CONFIG = {
    enabled: true,
    maxCheckpoints: 100,
    autoCheckpoint: true,
    autoCheckpointInterval: 300000, // 5 minutes
    checkpointOnDangerousOps: true,
    excludePatterns: [
        'node_modules/**',
        '.git/**',
        '*.log',
        '.env*',
        '*.lock',
        'dist/**',
        'build/**',
        '.next/**',
        '__pycache__/**',
        '*.pyc',
    ],
    maxFileSize: 10 * 1024 * 1024, // 10 MB
    compressCheckpoints: true,
};
const DANGEROUS_OPERATIONS = [
    'delete',
    'remove',
    'rm',
    'mv',
    'rename',
    'overwrite',
    'replace',
    'refactor',
    'rewrite',
];
/**
 * Checkpoint Manager
 *
 * Uses TypedEventEmitter for type-safe event handling.
 * Emits events: checkpoint:created, checkpoint:deleted, undo:noop, redo:noop,
 * undo:complete, redo:complete, restore:complete
 */
export class CheckpointManager extends TypedEventEmitter {
    config;
    dataDir;
    checkpointsDir;
    checkpoints = [];
    currentIndex = -1;
    workingDirectory;
    autoCheckpointTimer = null;
    dmp;
    constructor(workingDirectory, config = {}) {
        super();
        this.config = { ...DEFAULT_CONFIG, ...config };
        this.workingDirectory = workingDirectory;
        this.dataDir = path.join(os.homedir(), '.codebuddy', 'checkpoints');
        this.checkpointsDir = path.join(this.dataDir, this.hashPath(workingDirectory));
        this.dmp = new diff_match_patch();
        this.initialize();
    }
    /**
     * Initialize checkpoint manager
     */
    async initialize() {
        await fs.ensureDir(this.dataDir);
        await fs.ensureDir(this.checkpointsDir);
        await fs.ensureDir(path.join(this.checkpointsDir, 'files'));
        await this.loadCheckpoints();
        if (this.config.autoCheckpoint) {
            this.startAutoCheckpoint();
        }
    }
    /**
     * Hash a path for storage
     */
    hashPath(p) {
        return crypto.createHash('sha256').update(p).digest('hex').slice(0, 16);
    }
    /**
     * Load checkpoints from disk
     */
    async loadCheckpoints() {
        const indexPath = path.join(this.checkpointsDir, 'index.json');
        if (await fs.pathExists(indexPath)) {
            try {
                const data = await fs.readJSON(indexPath);
                this.checkpoints = data.checkpoints || [];
                this.currentIndex = data.currentIndex ?? (this.checkpoints.length - 1);
            }
            catch {
                this.checkpoints = [];
                this.currentIndex = -1;
            }
        }
    }
    /**
     * Save checkpoints index
     */
    async saveIndex() {
        const indexPath = path.join(this.checkpointsDir, 'index.json');
        await fs.writeJSON(indexPath, {
            workingDirectory: this.workingDirectory,
            checkpoints: this.checkpoints,
            currentIndex: this.currentIndex,
            updatedAt: new Date().toISOString(),
        }, { spaces: 2 });
    }
    /**
     * Create a new checkpoint
     */
    async createCheckpoint(options) {
        if (!this.config.enabled) {
            throw new Error('Checkpoint system is disabled. Enable it in settings with: codebuddy config set checkpoints.enabled true');
        }
        const id = crypto.randomBytes(8).toString('hex');
        const timestamp = new Date();
        // Get files to checkpoint
        const filesToCheckpoint = options.files || await this.getTrackedFiles();
        // Create file snapshots in parallel for better performance
        const snapshotResults = await Promise.allSettled(filesToCheckpoint.map(filePath => this.snapshotFile(filePath, id)));
        const files = snapshotResults
            .filter((result) => result.status === 'fulfilled' && result.value !== null)
            .map(result => result.value);
        // Get git info (async)
        const gitInfo = await this.getGitInfoAsync();
        const checkpoint = {
            id,
            name: options.name || `Checkpoint ${this.checkpoints.length + 1}`,
            description: options.description,
            timestamp,
            files,
            metadata: {
                workingDirectory: this.workingDirectory,
                gitBranch: gitInfo.branch,
                gitCommit: gitInfo.commit,
                operation: options.operation,
                tool: options.tool,
                automatic: options.automatic || false,
            },
            tags: options.tags || [],
            parentId: this.checkpoints.length > 0
                ? this.checkpoints[this.currentIndex]?.id
                : undefined,
        };
        // Remove any checkpoints after current index (for redo clearing)
        if (this.currentIndex < this.checkpoints.length - 1) {
            const removed = this.checkpoints.splice(this.currentIndex + 1);
            // Delete checkpoint files in parallel for better performance
            await Promise.allSettled(removed.map(cp => this.deleteCheckpointFiles(cp)));
        }
        this.checkpoints.push(checkpoint);
        this.currentIndex = this.checkpoints.length - 1;
        // Enforce max checkpoints
        await this.enforceMaxCheckpoints();
        await this.saveIndex();
        this.emit('checkpoint:created', {
            checkpoint: {
                id: checkpoint.id,
                name: checkpoint.name,
                timestamp: checkpoint.timestamp,
                files: checkpoint.files.map(f => ({ relativePath: f.relativePath })),
            },
        });
        return checkpoint;
    }
    /**
     * Snapshot a single file
     */
    async snapshotFile(filePath, checkpointId) {
        const fullPath = path.isAbsolute(filePath)
            ? filePath
            : path.join(this.workingDirectory, filePath);
        const relativePath = path.relative(this.workingDirectory, fullPath);
        // Check exclude patterns
        if (this.isExcluded(relativePath)) {
            return null;
        }
        const exists = await fs.pathExists(fullPath);
        if (!exists) {
            return {
                path: fullPath,
                relativePath,
                hash: '',
                size: 0,
                mode: 0,
                exists: false,
                isNew: false,
                isDeleted: true,
            };
        }
        const stats = await fs.stat(fullPath);
        // Skip large files
        if (stats.size > this.config.maxFileSize) {
            return null;
        }
        // Read and hash content
        const content = await fs.readFile(fullPath);
        const hash = crypto.createHash('sha256').update(content).digest('hex');
        // Save file content
        const storagePath = path.join(this.checkpointsDir, 'files', checkpointId, relativePath);
        await fs.ensureDir(path.dirname(storagePath));
        await fs.writeFile(storagePath, content);
        // Check if this is a new file
        const isNew = !this.checkpoints.some(cp => cp.files.some(f => f.relativePath === relativePath && f.exists));
        return {
            path: fullPath,
            relativePath,
            hash,
            size: stats.size,
            mode: stats.mode,
            exists: true,
            isNew,
            isDeleted: false,
        };
    }
    /**
     * Check if a path matches exclude patterns
     */
    isExcluded(relativePath) {
        for (const pattern of this.config.excludePatterns) {
            if (this.matchGlob(relativePath, pattern)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Simple glob matching
     */
    matchGlob(path, pattern) {
        const regexPattern = pattern
            .replace(/\*\*/g, '{{GLOBSTAR}}')
            .replace(/\*/g, '[^/]*')
            .replace(/\?/g, '[^/]')
            .replace(/{{GLOBSTAR}}/g, '.*');
        return new RegExp(`^${regexPattern}$`).test(path);
    }
    /**
     * Get tracked files in working directory
     */
    async getTrackedFiles() {
        const files = [];
        const walk = async (dir) => {
            const entries = await fs.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                const relativePath = path.relative(this.workingDirectory, fullPath);
                if (this.isExcluded(relativePath)) {
                    continue;
                }
                if (entry.isDirectory()) {
                    await walk(fullPath);
                }
                else if (entry.isFile()) {
                    files.push(fullPath);
                }
            }
        };
        await walk(this.workingDirectory);
        return files;
    }
    /**
     * Execute a git command asynchronously using spawn (secure)
     */
    execGitCommand(args) {
        return new Promise((resolve, reject) => {
            const proc = spawn('git', args, { cwd: this.workingDirectory });
            let stdout = '';
            let stderr = '';
            proc.stdout.on('data', (data) => { stdout += data.toString(); });
            proc.stderr.on('data', (data) => { stderr += data.toString(); });
            proc.on('close', (code) => {
                if (code === 0) {
                    resolve(stdout.trim());
                }
                else {
                    reject(new Error(stderr || `git command failed with code ${code}`));
                }
            });
            proc.on('error', reject);
        });
    }
    /**
     * Get git info (async version)
     */
    async getGitInfoAsync() {
        try {
            const [branch, commit] = await Promise.all([
                this.execGitCommand(['rev-parse', '--abbrev-ref', 'HEAD']),
                this.execGitCommand(['rev-parse', '--short', 'HEAD']),
            ]);
            return { branch, commit };
        }
        catch {
            return {};
        }
    }
    /**
     * Get git info (sync wrapper for compatibility - prefers cached value)
     */
    getGitInfo() {
        // Return empty object synchronously, actual git info is fetched async in createCheckpoint
        return {};
    }
    /**
     * Undo to previous checkpoint
     */
    async undo() {
        if (this.currentIndex <= 0) {
            this.emit('undo:noop', { reason: 'No previous checkpoint' });
            return null;
        }
        const targetCheckpoint = this.checkpoints[this.currentIndex - 1];
        return this.restoreCheckpoint(targetCheckpoint, 'undo');
    }
    /**
     * Redo to next checkpoint
     */
    async redo() {
        if (this.currentIndex >= this.checkpoints.length - 1) {
            this.emit('redo:noop', { reason: 'No next checkpoint' });
            return null;
        }
        const targetCheckpoint = this.checkpoints[this.currentIndex + 1];
        return this.restoreCheckpoint(targetCheckpoint, 'redo');
    }
    /**
     * Restore a specific checkpoint
     */
    async restoreCheckpoint(checkpoint, operation) {
        const restoredFiles = [];
        const errors = [];
        // Create a safety checkpoint before restore
        await this.createCheckpoint({
            name: `Before ${operation}`,
            operation: operation,
            automatic: true,
        });
        for (const file of checkpoint.files) {
            try {
                const storagePath = path.join(this.checkpointsDir, 'files', checkpoint.id, file.relativePath);
                const targetPath = path.join(this.workingDirectory, file.relativePath);
                if (file.isDeleted || !file.exists) {
                    // File was deleted at this checkpoint
                    if (await fs.pathExists(targetPath)) {
                        await fs.remove(targetPath);
                        restoredFiles.push(file.relativePath);
                    }
                }
                else {
                    // Restore file content
                    await fs.ensureDir(path.dirname(targetPath));
                    if (await fs.pathExists(storagePath)) {
                        await fs.copy(storagePath, targetPath);
                        await fs.chmod(targetPath, file.mode);
                        restoredFiles.push(file.relativePath);
                    }
                }
            }
            catch (error) {
                errors.push({
                    path: file.relativePath,
                    error: getErrorMessage(error),
                });
            }
        }
        // Update current index
        const checkpointIndex = this.checkpoints.findIndex(c => c.id === checkpoint.id);
        if (checkpointIndex !== -1) {
            this.currentIndex = checkpointIndex;
        }
        await this.saveIndex();
        const result = {
            success: errors.length === 0,
            checkpoint,
            restoredFiles,
            errors,
        };
        // Emit typed event based on operation
        const eventPayload = {
            success: result.success,
            checkpoint: { id: checkpoint.id, name: checkpoint.name },
            restoredFiles: result.restoredFiles,
            errors: result.errors,
        };
        if (operation === 'undo') {
            this.emit('undo:complete', eventPayload);
        }
        else if (operation === 'redo') {
            this.emit('redo:complete', eventPayload);
        }
        else {
            this.emit('restore:complete', eventPayload);
        }
        return result;
    }
    /**
     * Get diff between two checkpoints
     */
    async getDiff(fromId, toId) {
        const fromCheckpoint = this.checkpoints.find(c => c.id === fromId);
        const toCheckpoint = this.checkpoints.find(c => c.id === toId);
        if (!fromCheckpoint || !toCheckpoint) {
            throw new Error('Checkpoint not found. Use listCheckpoints() to see available checkpoints.');
        }
        const changes = [];
        // Create maps for easier lookup
        const fromFiles = new Map(fromCheckpoint.files.map(f => [f.relativePath, f]));
        const toFiles = new Map(toCheckpoint.files.map(f => [f.relativePath, f]));
        // Find all unique paths
        const allPaths = new Set([
            ...fromFiles.keys(),
            ...toFiles.keys(),
        ]);
        for (const relativePath of allPaths) {
            const fromFile = fromFiles.get(relativePath);
            const toFile = toFiles.get(relativePath);
            let change = null;
            if (!fromFile && toFile && toFile.exists) {
                // File was created
                const content = await this.getCheckpointFileContent(toCheckpoint.id, relativePath);
                change = {
                    path: relativePath,
                    type: 'created',
                    newContent: content,
                };
            }
            else if (fromFile && fromFile.exists && (!toFile || !toFile.exists)) {
                // File was deleted
                const content = await this.getCheckpointFileContent(fromCheckpoint.id, relativePath);
                change = {
                    path: relativePath,
                    type: 'deleted',
                    oldContent: content,
                };
            }
            else if (fromFile && toFile && fromFile.hash !== toFile.hash) {
                // File was modified
                const oldContent = await this.getCheckpointFileContent(fromCheckpoint.id, relativePath);
                const newContent = await this.getCheckpointFileContent(toCheckpoint.id, relativePath);
                const diffs = this.dmp.diff_main(oldContent || '', newContent || '');
                this.dmp.diff_cleanupSemantic(diffs);
                const patchText = this.dmp.patch_toText(this.dmp.patch_make(oldContent || '', diffs));
                change = {
                    path: relativePath,
                    type: 'modified',
                    oldContent,
                    newContent,
                    diff: patchText,
                };
            }
            if (change) {
                changes.push(change);
            }
        }
        return changes;
    }
    /**
     * Get file content from checkpoint
     */
    async getCheckpointFileContent(checkpointId, relativePath) {
        const storagePath = path.join(this.checkpointsDir, 'files', checkpointId, relativePath);
        try {
            return await fs.readFile(storagePath, 'utf-8');
        }
        catch {
            return undefined;
        }
    }
    /**
     * Delete checkpoint files
     */
    async deleteCheckpointFiles(checkpoint) {
        const filesDir = path.join(this.checkpointsDir, 'files', checkpoint.id);
        await fs.remove(filesDir);
    }
    /**
     * Enforce maximum checkpoints
     */
    async enforceMaxCheckpoints() {
        while (this.checkpoints.length > this.config.maxCheckpoints) {
            const oldest = this.checkpoints.shift();
            await this.deleteCheckpointFiles(oldest);
            if (this.currentIndex > 0) {
                this.currentIndex--;
            }
        }
        // Ensure index stays within bounds
        if (this.currentIndex >= this.checkpoints.length) {
            this.currentIndex = Math.max(0, this.checkpoints.length - 1);
        }
    }
    /**
     * Check if operation should trigger auto-checkpoint
     */
    shouldAutoCheckpoint(operation) {
        if (!this.config.checkpointOnDangerousOps)
            return false;
        const lowerOp = operation.toLowerCase();
        return DANGEROUS_OPERATIONS.some(op => lowerOp.includes(op));
    }
    /**
     * Start auto-checkpoint timer
     */
    startAutoCheckpoint() {
        this.autoCheckpointTimer = setInterval(async () => {
            try {
                await this.createCheckpoint({
                    name: 'Auto checkpoint',
                    operation: 'auto',
                    automatic: true,
                });
            }
            catch {
                // Ignore auto-checkpoint errors
            }
        }, this.config.autoCheckpointInterval);
    }
    /**
     * Stop auto-checkpoint timer
     */
    stopAutoCheckpoint() {
        if (this.autoCheckpointTimer) {
            clearInterval(this.autoCheckpointTimer);
            this.autoCheckpointTimer = null;
        }
    }
    /**
     * Get all checkpoints
     */
    getCheckpoints() {
        return [...this.checkpoints];
    }
    /**
     * Get current checkpoint
     */
    getCurrentCheckpoint() {
        return this.checkpoints[this.currentIndex] || null;
    }
    /**
     * Get checkpoint by ID
     */
    getCheckpoint(id) {
        return this.checkpoints.find(c => c.id === id);
    }
    /**
     * Search checkpoints by tag or name
     */
    searchCheckpoints(query) {
        const lowerQuery = query.toLowerCase();
        return this.checkpoints.filter(c => c.name.toLowerCase().includes(lowerQuery) ||
            c.tags.some(t => t.toLowerCase().includes(lowerQuery)) ||
            c.description?.toLowerCase().includes(lowerQuery));
    }
    /**
     * Tag a checkpoint
     */
    async tagCheckpoint(id, tag) {
        const checkpoint = this.checkpoints.find(c => c.id === id);
        if (!checkpoint) {
            throw new Error('Checkpoint not found. Use listCheckpoints() to see available checkpoints.');
        }
        if (!checkpoint.tags.includes(tag)) {
            checkpoint.tags.push(tag);
            await this.saveIndex();
        }
    }
    /**
     * Rename a checkpoint
     */
    async renameCheckpoint(id, name) {
        const checkpoint = this.checkpoints.find(c => c.id === id);
        if (!checkpoint) {
            throw new Error('Checkpoint not found. Use listCheckpoints() to see available checkpoints.');
        }
        checkpoint.name = name;
        await this.saveIndex();
    }
    /**
     * Delete a checkpoint
     */
    async deleteCheckpoint(id) {
        const index = this.checkpoints.findIndex(c => c.id === id);
        if (index === -1) {
            throw new Error('Checkpoint not found. Use listCheckpoints() to see available checkpoints.');
        }
        const checkpoint = this.checkpoints[index];
        await this.deleteCheckpointFiles(checkpoint);
        this.checkpoints.splice(index, 1);
        if (this.currentIndex >= index) {
            this.currentIndex = Math.max(0, this.currentIndex - 1);
        }
        await this.saveIndex();
        this.emit('checkpoint:deleted', { id });
    }
    /**
     * Can undo
     */
    canUndo() {
        return this.currentIndex > 0;
    }
    /**
     * Can redo
     */
    canRedo() {
        return this.currentIndex < this.checkpoints.length - 1;
    }
    /**
     * Format status
     */
    formatStatus() {
        const current = this.getCurrentCheckpoint();
        const lines = [
            '╔══════════════════════════════════════════════════════════════╗',
            '║                    ↩️  CHECKPOINT MANAGER                     ║',
            '╠══════════════════════════════════════════════════════════════╣',
            `║ Total Checkpoints: ${this.checkpoints.length.toString().padEnd(40)}║`,
            `║ Current Position:  ${(this.currentIndex + 1)}/${this.checkpoints.length}${''.padEnd(37)}║`,
            `║ Can Undo:          ${this.canUndo() ? '✅ Yes' : '❌ No'}${''.padEnd(35)}║`,
            `║ Can Redo:          ${this.canRedo() ? '✅ Yes' : '❌ No'}${''.padEnd(35)}║`,
            '╠══════════════════════════════════════════════════════════════╣',
        ];
        if (current) {
            lines.push('║ CURRENT CHECKPOINT                                           ║');
            lines.push(`║ Name: ${current.name.slice(0, 50).padEnd(53)}║`);
            lines.push(`║ Time: ${new Date(current.timestamp).toLocaleString().padEnd(53)}║`);
            lines.push(`║ Files: ${current.files.length.toString().padEnd(52)}║`);
        }
        lines.push('╠══════════════════════════════════════════════════════════════╣');
        // Show recent checkpoints
        const recent = this.checkpoints.slice(-5).reverse();
        if (recent.length > 0) {
            lines.push('║ RECENT CHECKPOINTS                                           ║');
            for (const cp of recent) {
                const marker = cp.id === current?.id ? '→' : ' ';
                const auto = cp.metadata.automatic ? '🤖' : '👤';
                lines.push(`║ ${marker} ${auto} ${cp.name.slice(0, 45).padEnd(45)} ${cp.files.length} files ║`);
            }
        }
        lines.push('╠══════════════════════════════════════════════════════════════╣');
        lines.push('║ /undo | /redo | /checkpoint create <name>                    ║');
        lines.push('║ /checkpoint list | /checkpoint restore <id>                  ║');
        lines.push('╚══════════════════════════════════════════════════════════════╝');
        return lines.join('\n');
    }
    /**
     * Dispose and clean up resources
     */
    dispose() {
        this.stopAutoCheckpoint();
        this.saveIndex();
        // Use TypedEventEmitter's dispose method to clean up listeners and history
        super.dispose();
    }
}
// Factory function
export function createCheckpointManager(workingDirectory, config) {
    return new CheckpointManager(workingDirectory, config);
}
//# sourceMappingURL=checkpoint-manager.js.map