/**
 * Edit Command - File editing with AI assistance
 */
import chalk from 'chalk';
import inquirer from 'inquirer';
import { createSpinner } from '../utils/logger.js';
import { APIClient, CLIError, classifyError, formatCLIError } from '../utils/api.js';
import { FileUtils } from '../utils/files.js';
export class EditCommand {
    config;
    logger;
    api;
    fileUtils;
    constructor(config, logger) {
        this.config = config;
        this.logger = logger;
        this.api = new APIClient(config, logger);
        this.fileUtils = new FileUtils(process.cwd(), config.get('project').ignorePatterns);
    }
    async run(filePath, options) {
        // Check auth
        if (!this.config.isAuthenticated()) {
            this.logger.error('Not authenticated. Run: vigthoria login');
            return;
        }
        // Server-side token validation — fail fast instead of waiting for 401
        const tokenCheck = await this.api.validateToken();
        if (!tokenCheck.valid) {
            this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
            return;
        }
        // Read file
        const file = this.fileUtils.readFile(filePath);
        if (!file) {
            this.logger.error(`File not found: ${filePath}`);
            return;
        }
        this.logger.section(`Editing: ${file.relativePath}`);
        console.log(chalk.gray(`Language: ${file.language} | Lines: ${file.lines}`));
        console.log();
        // Get instruction
        let instruction = options.instruction;
        if (!instruction) {
            // --apply requires --instruction to avoid hanging on non-interactive terminals
            if (options.apply) {
                this.logger.error('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"');
                return;
            }
            // Non-TTY stdin cannot prompt interactively
            if (!process.stdin.isTTY) {
                this.logger.error('No --instruction provided and stdin is not interactive. Use: vigthoria edit file.ts --instruction "..."');
                return;
            }
            const answer = await inquirer.prompt([
                {
                    type: 'input',
                    name: 'instruction',
                    message: 'What changes would you like to make?',
                    validate: (input) => input.length > 0 || 'Please provide an instruction',
                },
            ]);
            instruction = answer.instruction;
        }
        // Generate edit
        const spinner = createSpinner({
            text: 'Generating changes...',
            spinner: 'dots',
        }).start();
        try {
            const response = await this.api.chat([
                {
                    role: 'system',
                    content: `You are a precise code editor. Edit the provided code EXACTLY as the user instructs.

MANDATORY RULES:
1. Return ONLY the complete modified file content. No explanation, no markdown.
2. Preserve all original formatting, indentation, and style.
3. Make ONLY the changes the instruction asks for — nothing more, nothing less.
4. If the instruction says "Replace X with Y", then X must become Y verbatim.
5. Do NOT add, remove, or rewrite lines the instruction didn't mention.
6. Do NOT summarize, shorten, paraphrase, or "improve" the requested text.
7. The instruction text is LITERAL — reproduce it character-for-character.`,
                },
                {
                    role: 'user',
                    content: `File: ${file.relativePath}
Language: ${file.language}

Original code:
\`\`\`${file.language}
${file.content}
\`\`\`

Instruction: ${instruction}

Return the complete modified file content:`,
                },
            ], options.model);
            spinner.stop();
            // Extract code from response
            let modifiedCode = this.extractCode(response.message, file.language);
            if (!modifiedCode) {
                this.logger.error('Failed to generate valid code changes');
                return;
            }
            // Always deduplicate — extractCode only dedupes on the no-fence
            // fallback path, so fenced responses (the common case) need this.
            modifiedCode = this.deduplicateCode(modifiedCode);
            // Show diff and apply
            if (options.apply) {
                await this.applyFix(file.path, file.content, modifiedCode);
            }
            else {
                await this.showDiffAndConfirm(file.path, file.content, modifiedCode);
            }
        }
        catch (error) {
            spinner.stop();
            const cliErr = error instanceof CLIError ? error : classifyError(error);
            this.logger.error(formatCLIError(cliErr));
        }
    }
    async fix(filePath, options) {
        // Check auth
        if (!this.config.isAuthenticated()) {
            this.logger.error('Not authenticated. Run: vigthoria login');
            return;
        }
        // Server-side token validation — fail fast instead of waiting for 401
        const tokenCheck = await this.api.validateToken();
        if (!tokenCheck.valid) {
            this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
            return;
        }
        // Read file
        const file = this.fileUtils.readFile(filePath);
        if (!file) {
            this.logger.error(`File not found: ${filePath}`);
            return;
        }
        this.logger.section(`Fixing: ${file.relativePath}`);
        console.log(chalk.gray(`Fix type: ${options.type} | Language: ${file.language}`));
        console.log();
        const spinner = createSpinner({
            text: `Analyzing for ${options.type} issues...`,
            spinner: 'dots',
        }).start();
        try {
            const result = await this.api.fixCode(file.content, file.language, options.type);
            spinner.stop();
            // Deduplicate the fixed output — models sometimes repeat code 2-3x
            result.fixed = this.deduplicateCode(result.fixed);
            if (result.changes.length === 0) {
                // Even if the diff engine found no structured changes, the fixed
                // code may still differ (e.g. single-char operator swap).  Show a
                // diff and let the user decide instead of silently discarding.
                if (result.fixed && result.fixed !== file.content) {
                    this.logger.section('Found 1 issue(s)');
                    console.log(chalk.yellow('1. Operator/character-level fix detected'));
                    console.log();
                    if (options.apply) {
                        await this.applyFix(file.path, file.content, result.fixed);
                    }
                    else {
                        await this.showDiffAndConfirm(file.path, file.content, result.fixed);
                    }
                    return;
                }
                this.logger.success('No issues found!');
                return;
            }
            // Show fixes
            this.logger.section(`Found ${result.changes.length} issue(s)`);
            result.changes.forEach((change, i) => {
                console.log(chalk.yellow(`${i + 1}. Line ${change.line}:`));
                console.log(chalk.red(`   - ${change.before}`));
                console.log(chalk.green(`   + ${change.after}`));
                console.log(chalk.gray(`   Reason: ${change.reason}`));
                console.log();
            });
            // Apply or confirm
            if (options.apply) {
                await this.applyFix(file.path, file.content, result.fixed);
            }
            else {
                await this.showDiffAndConfirm(file.path, file.content, result.fixed);
            }
        }
        catch (error) {
            spinner.stop();
            const cliErr = error instanceof CLIError ? error : classifyError(error);
            this.logger.error(formatCLIError(cliErr));
        }
    }
    extractCode(response, language) {
        // Try to extract code block (language-specific first, then any)
        const codeBlockRegex = new RegExp(`\`\`\`(?:${language})?\\n([\\s\\S]*?)\`\`\``, 'i');
        const match = response.match(codeBlockRegex);
        if (match) {
            return match[1].trim();
        }
        // Try generic code block
        const genericMatch = response.match(/```\w*\n([\s\S]*?)```/);
        if (genericMatch) {
            return genericMatch[1].trim();
        }
        // If no code block, check if response looks like code
        const trimmed = response.trim();
        if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
            return this.deduplicateCode(trimmed);
        }
        return null;
    }
    /**
     * Detect and remove duplicated content in model output.
     * Models sometimes output the code twice or three times.
     * Applies iteratively until no further duplicates are found.
     */
    deduplicateCode(code) {
        let result = code;
        // Iterate up to 3 times to handle triple+ duplication
        for (let pass = 0; pass < 3; pass++) {
            const deduped = this.deduplicateOnce(result);
            if (deduped === result)
                break;
            result = deduped;
        }
        // Safety net: if dedup accidentally removed structural delimiters,
        // re-balance braces/parens/brackets by appending missing closers.
        if (result !== code) {
            result = this.repairBrackets(result);
        }
        return result;
    }
    /**
     * Append missing closing delimiters if dedup left brackets unbalanced.
     */
    repairBrackets(code) {
        const stack = [];
        const closer = { '{': '}', '(': ')', '[': ']' };
        const openers = new Set(Object.keys(closer));
        const closers = new Set(Object.values(closer));
        let inString = null;
        let escaped = false;
        for (const ch of code) {
            if (escaped) {
                escaped = false;
                continue;
            }
            if (ch === '\\') {
                escaped = true;
                continue;
            }
            if (inString) {
                if (ch === inString)
                    inString = null;
                continue;
            }
            if (ch === '"' || ch === "'" || ch === '`') {
                inString = ch;
                continue;
            }
            if (openers.has(ch))
                stack.push(ch);
            else if (closers.has(ch)) {
                if (stack.length > 0 && closer[stack[stack.length - 1]] === ch) {
                    stack.pop();
                }
            }
        }
        if (stack.length === 0)
            return code;
        // Append missing closers in reverse order (innermost first)
        const suffix = stack.reverse().map(o => closer[o]).join('\n');
        return code + '\n' + suffix;
    }
    deduplicateOnce(code) {
        const lines = code.split('\n');
        const len = lines.length;
        if (len < 4)
            return code;
        // Pass 1: Remove model stutter — consecutive runs of 3+ identical
        // non-empty lines are collapsed to one.  A pair (exactly 2) is only
        // collapsed when it occurs at the very end of the output (trailing
        // stutter).  Pairs in the middle are kept — they may be intentional
        // (e.g. repeated data rows, CSS rules).
        const deduped = [];
        let i = 0;
        while (i < lines.length) {
            const trimmed = lines[i].trim();
            if (!trimmed) {
                deduped.push(lines[i]);
                i++;
                continue;
            }
            // Count the run length of identical consecutive lines.
            // For runs of 3+, trimmed comparison is fine (clear stutter).
            // For trailing pairs, require EXACT match (including indentation)
            // so that e.g. "  }" and "}" (different blocks) are not collapsed.
            let runEnd = i + 1;
            while (runEnd < lines.length && lines[runEnd].trim() === trimmed) {
                runEnd++;
            }
            const runLen = runEnd - i;
            // Check if this run is "effectively at end" — only trailing empty
            // lines follow the duplicate pair.
            const isAtEffectiveEnd = runEnd === lines.length
                || lines.slice(runEnd).every(l => l.trim() === '');
            if (runLen >= 3) {
                // 3+ identical (trimmed) lines is almost certainly stutter — keep one
                deduped.push(lines[i]);
            }
            else if (runLen === 2 && isAtEffectiveEnd && lines[i].trimEnd() === lines[i + 1].trimEnd()) {
                // Exactly 2 identical lines (same leading indent) at end
                deduped.push(lines[i].trimEnd());
            }
            else {
                // 1 line, or a pair in the middle — keep all
                for (let j = i; j < runEnd; j++) {
                    deduped.push(lines[j]);
                }
            }
            i = runEnd;
        }
        if (deduped.length < lines.length) {
            return deduped.join('\n');
        }
        // Pass 2: Check if the second half is a near-duplicate of the first half
        for (let splitAt = Math.floor(len * 0.4); splitAt <= Math.ceil(len * 0.6); splitAt++) {
            const firstHalf = lines.slice(0, splitAt);
            const secondHalf = lines.slice(splitAt).filter(l => l.trim() !== '');
            if (secondHalf.length < 2)
                continue;
            let matches = 0;
            for (const line of secondHalf) {
                if (firstHalf.some(fl => fl.trim() === line.trim()))
                    matches++;
            }
            // If >70% of second half matches first half, it's a duplicate — keep second half
            if (matches / secondHalf.length > 0.7) {
                return secondHalf.join('\n');
            }
        }
        return code;
    }
    async showDiffAndConfirm(filePath, original, modified) {
        const diff = this.fileUtils.createDiff(original, modified);
        if (diff.added.length === 0 && diff.removed.length === 0) {
            this.logger.info('No changes detected');
            return;
        }
        // Show diff
        this.logger.section('Changes');
        diff.removed.forEach(line => {
            console.log(chalk.red(line));
        });
        diff.added.forEach(line => {
            console.log(chalk.green(line));
        });
        console.log();
        // Confirm
        const showDiffs = this.config.get('preferences').showDiffs;
        const autoApply = this.config.get('preferences').autoApplyFixes;
        if (autoApply) {
            await this.applyFix(filePath, original, modified);
            return;
        }
        // Non-TTY: show the diff but don't try to prompt — re-run with --apply
        if (!process.stdin.isTTY) {
            this.logger.info('Non-interactive mode. Re-run with --apply to apply changes.');
            return;
        }
        const { action } = await inquirer.prompt([
            {
                type: 'list',
                name: 'action',
                message: 'What would you like to do?',
                choices: [
                    { name: 'Apply changes', value: 'apply' },
                    { name: 'View full diff', value: 'view' },
                    { name: 'Discard changes', value: 'discard' },
                ],
            },
        ]);
        switch (action) {
            case 'apply':
                await this.applyFix(filePath, original, modified);
                break;
            case 'view':
                this.showFullDiff(original, modified);
                const { confirm } = await inquirer.prompt([
                    {
                        type: 'confirm',
                        name: 'confirm',
                        message: 'Apply these changes?',
                        default: true,
                    },
                ]);
                if (confirm) {
                    await this.applyFix(filePath, original, modified);
                }
                break;
            case 'discard':
                this.logger.info('Changes discarded');
                break;
        }
    }
    showFullDiff(original, modified) {
        const originalLines = original.split('\n');
        const modifiedLines = modified.split('\n');
        console.log();
        console.log(chalk.gray('─'.repeat(60)));
        // Use LCS-based diff to avoid line-shift inflation
        const m = originalLines.length;
        const n = modifiedLines.length;
        const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
        for (let i = 1; i <= m; i++) {
            for (let j = 1; j <= n; j++) {
                if (originalLines[i - 1] === modifiedLines[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        // Backtrack to produce diff ops
        const ops = [];
        let i = m, j = n;
        while (i > 0 || j > 0) {
            if (i > 0 && j > 0 && originalLines[i - 1] === modifiedLines[j - 1]) {
                ops.push({ type: 'keep', text: originalLines[i - 1], lineNum: i });
                i--;
                j--;
            }
            else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
                ops.push({ type: 'add', text: modifiedLines[j - 1], lineNum: j });
                j--;
            }
            else {
                ops.push({ type: 'remove', text: originalLines[i - 1], lineNum: i });
                i--;
            }
        }
        ops.reverse();
        let displayLine = 0;
        for (const op of ops) {
            if (op.type === 'keep') {
                displayLine++;
                const lineNum = String(displayLine).padStart(4, ' ');
                console.log(chalk.gray(`${lineNum} │ ${op.text || ''}`));
            }
            else if (op.type === 'remove') {
                displayLine++;
                const lineNum = String(displayLine).padStart(4, ' ');
                console.log(chalk.red(`${lineNum} - ${op.text}`));
            }
            else {
                const lineNum = '   +';
                console.log(chalk.green(`${lineNum} + ${op.text}`));
            }
        }
        console.log(chalk.gray('─'.repeat(60)));
        console.log();
    }
    async applyFix(filePath, original, modified) {
        // Create backup
        const backup = this.fileUtils.backupFile(filePath);
        if (backup) {
            this.logger.info(`Backup: ${chalk.gray(backup)}`);
        }
        // Apply changes
        if (this.fileUtils.writeFile(filePath, modified)) {
            this.logger.success(`Changes applied to ${filePath}`);
        }
        else {
            this.logger.error('Failed to write changes');
        }
    }
}
