/**
 * Code action provider for Acurity Architect.
 * Provides quick fixes for various diagnostics.
 */

import * as vscode from "vscode";
import { LANGUAGE_ID } from "../../core/src/constants.js";
import { getQuickFixMessageId } from "../../core/src/diagnostics/quickFixCatalog.js";
import { isSuppressable, getCatalogEntry } from "../../core/src/diagnostics/diagnosticCatalog.js";

/** Command ID for inserting a @rule-ignore snippet above the offending line. */
const SUPPRESS_COMMAND_ID = "acurity.suppressDiagnostic";

/**
 * Registers code action providers and the diagnostic suppression command.
 * @param {vscode.ExtensionContext} context - The extension context
 */
function registerCodeActions(context) {
    context.subscriptions.push(
        vscode.languages.registerCodeActionsProvider(LANGUAGE_ID, new KeywordCapitalizationCodeActionProvider(), {
            providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
        })
    );

    // Command executed by the "Suppress this line" quick action.
    // Uses insertSnippet so the cursor lands inside the REASON placeholder.
    context.subscriptions.push(
        vscode.commands.registerCommand(SUPPRESS_COMMAND_ID, async (uri, lineIndex, messageId) => {
            const editor = vscode.window.visibleTextEditors.find(
                (e) => e.document.uri.toString() === uri.toString()
            );
            if (!editor) return;

            const line = editor.document.lineAt(lineIndex);
            const indent = line.text.match(/^(\s*)/)[1];
            const insertPos = new vscode.Position(lineIndex, 0);

            await editor.insertSnippet(
                new vscode.SnippetString(`${indent}// @rule-ignore ${messageId} REASON: \${1:<reason>}\n`),
                insertPos
            );
        })
    );
}

/**
 * Returns the ruleId for a diagnostic using the catalog, keyed by diagnostic.code (the messageId).
 * diagnostic.data is not used here because it does not survive VS Code's internal diagnostic
 * serialization — context.diagnostics in provideCodeActions is reconstructed from IMarkerData
 * (renderer-side), which does not include the data field.
 * @param {vscode.Diagnostic} diagnostic
 * @returns {string|undefined}
 */
function getRuleId(diagnostic) {
    return getCatalogEntry(diagnostic.code)?.ruleId;
}

/**
 * Code action provider for keyword capitalization fixes.
 */
class KeywordCapitalizationCodeActionProvider {
    provideCodeActions(document, range, context) {
        const codeActions = [];

        // VS Code does not always populate context.diagnostics (e.g. when the
        // lightbulb fires asynchronously after validation completes). Fall back to
        // querying the live diagnostic collection and filtering to the cursor range.
        //
        // IMPORTANT: vscode.languages.getDiagnostics() returns plain IPC-deserialized
        // objects, NOT vscode.Range instances. Calling d.range.intersection() directly
        // will throw "is not a function" at runtime, which VS Code swallows silently —
        // the provider returns no actions with no error visible in the UI or logs.
        // Always reconstruct a vscode.Range from the raw coordinates before use.
        const diagnostics =
            context.diagnostics.length > 0
                ? context.diagnostics
                : document?.uri
                      ? vscode.languages
                            .getDiagnostics(document.uri)
                            .filter((d) => {
                                const dRange = new vscode.Range(
                                    d.range.start.line, d.range.start.character,
                                    d.range.end.line, d.range.end.character
                                );
                                return dRange.intersection(range) !== undefined;
                            })
                      : [];

        // Check each diagnostic in the context
        diagnostics.forEach((diagnostic) => {
            if (getRuleId(diagnostic) === "keywordCapitalization") {
                const action = this.createKeywordCapitalizationFix(document, diagnostic);
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "typeKeywordCapitalization") {
                const action = this.createTypeKeywordCapitalizationFix(document, diagnostic);
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "unclosedString") {
                const action = this.createUnclosedStringFix(document, diagnostic);
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "interpolationUnclosed") {
                const action = this.createUnclosedInterpolationFix(document, diagnostic);
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "interpolationCaseMismatch") {
                const actions = this.createInterpolationCaseMismatchFixes(document, diagnostic);
                if (actions.length > 0) {
                    codeActions.push(...actions);
                }
            } else if (getRuleId(diagnostic) === "functionCaseMismatch") {
                const actions = this.createFunctionCaseMismatchFixes(document, diagnostic);
                if (actions.length > 0) {
                    codeActions.push(...actions);
                }
            } else if (getRuleId(diagnostic) === "namingDatabaseFieldDeclaration") {
                const action = this.createRemoveDatabaseFieldPrefixFix(document, diagnostic);
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "namingPrefixType") {
                const action = this.createVariableRenameFix(document, diagnostic, "namingPrefixType");
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "namingPrefixScope") {
                const action = this.createVariableRenameFix(document, diagnostic, "namingPrefixScope");
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "indexUsageSaveRestoreRecommendation") {
                // ACU-PAT-003: No SAVE present — offer to insert SAVE before OPEN
                const actions = this.createIndexSaveRestoreFixes(document, diagnostic);
                codeActions.push(...actions);
            } else if (getRuleId(diagnostic) === "indexUsageSaveRestorePair") {
                // ACU-PAT-002: SAVE without RESTORE — offer to insert RESTORE after CLOSE
                const action = this.createIndexRestorePositionFix(document, diagnostic);
                if (action) {
                    codeActions.push(action);
                }
            } else if (getRuleId(diagnostic) === "discouragedIntegerType") {
                const action = this.createDiscouragedIntegerTypeFix(document, diagnostic);
                if (action) codeActions.push(action);
            } else if (getRuleId(diagnostic) === "functionCallWhitespace") {
                const action = this.createFunctionWhitespaceFix(document, diagnostic);
                if (action) codeActions.push(action);
            } else if (getRuleId(diagnostic) === "declaredButNeverUsedVariable") {
                const action = this.createUnusedVariableDeclarationFix(document, diagnostic);
                if (action) codeActions.push(action);
            } else if (getRuleId(diagnostic) === "exitTerminatorIssue") {
                // ACU-STY-008: distinguish redundant vs deprecated from the rendered message.
                // Both redundant and deprecated can appear on END-OF-CODE lines, so line
                // content alone cannot distinguish them; the message text is reliable.
                const variant = /deprecated/i.test(diagnostic.message) ? "deprecated" : "redundant";
                if (variant === "redundant") {
                    const action = this.createRedundantTerminatorFix(document, diagnostic);
                    if (action) codeActions.push(action);
                } else if (variant === "deprecated") {
                    const action = this.createDeprecatedEndOfCodeFix(document, diagnostic);
                    if (action) codeActions.push(action);
                }
            }

            // Offer "Suppress this line" for every suppressable diagnostic
            const suppressAction = this.createSuppressionCommentFix(document, diagnostic);
            if (suppressAction) {
                codeActions.push(suppressAction);
            }
        });

        return codeActions;
    }

    /**
     * Creates a quick fix code action for keyword capitalization.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction|null} - The code action or null
     */
    createKeywordCapitalizationFix(document, diagnostic) {
        const line = document.lineAt(diagnostic.range.start.line);
        const lineText = line.text;

        // Extract the keyword from the diagnostic message
        // Message format: "Keyword 'if' should be capitalized as 'IF'."
        const messageMatch = diagnostic.message.match(/Keyword '([^']+)' should be capitalized as '([^']+)'/);
        if (!messageMatch) {
            return null;
        }

        const lowercase = messageMatch[1];
        const uppercase = messageMatch[2];

        // Find the exact position of the keyword in the line
        const keywordStart = diagnostic.range.start.character;
        const keywordEnd = diagnostic.range.end.character;

        // Validate that the text at this range actually matches the expected lowercase keyword
        // This prevents applying fixes when diagnostic positions are stale after previous edits
        const actualText = lineText.substring(keywordStart, keywordEnd);
        if (actualText !== lowercase) {
            return null; // Position is stale, skip this fix
        }

        // Create a CodeAction to fix this diagnostic
        const fix = new vscode.CodeAction(`Capitalize '${lowercase}' to '${uppercase}'`, vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();

        const range = new vscode.Range(
            diagnostic.range.start.line,
            keywordStart,
            diagnostic.range.start.line,
            keywordEnd
        );

        fix.edit.replace(document.uri, range, uppercase);
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Creates a quick fix code action for type keyword capitalization.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction|null} - The code action or null
     */
    createTypeKeywordCapitalizationFix(document, diagnostic) {
        const line = document.lineAt(diagnostic.range.start.line);
        const lineText = line.text;

        // Extract the type from the diagnostic message
        // Message format: "Type keyword 'string' should be capitalized as 'STRING'."
        const messageMatch = diagnostic.message.match(/Type keyword '([^']+)' should be capitalized as '([^']+)'/);
        if (!messageMatch) {
            return null;
        }

        const lowercase = messageMatch[1];
        const uppercase = messageMatch[2];

        // Find the exact position of the type keyword in the line
        const typeStart = diagnostic.range.start.character;
        const typeEnd = diagnostic.range.end.character;

        // Validate that the text at this range actually matches the expected lowercase keyword
        // This prevents applying fixes when diagnostic positions are stale after previous edits
        const actualText = lineText.substring(typeStart, typeEnd);
        if (actualText !== lowercase) {
            return null; // Position is stale, skip this fix
        }

        // Create a CodeAction to fix this diagnostic
        const fix = new vscode.CodeAction(`Capitalize '${lowercase}' to '${uppercase}'`, vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();

        const range = new vscode.Range(
            diagnostic.range.start.line,
            typeStart,
            diagnostic.range.start.line,
            typeEnd
        );

        fix.edit.replace(document.uri, range, uppercase);
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Creates a quick fix code action for unclosed strings.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction|null} - The code action or null
     */
    createUnclosedStringFix(document, diagnostic) {
        const line = document.lineAt(diagnostic.range.start.line);
        const lineText = line.text;

        // Find the unclosed string by looking for opening quotes after the diagnostic position
        // Search backward from the diagnostic end to find the opening quote
        const searchStart = Math.min(diagnostic.range.end.character, lineText.length);
        
        let quoteChar = null;

        // Look for the most recent unclosed quote before the diagnostic position
        for (let i = searchStart - 1; i >= 0; i -= 1) {
            const char = lineText[i];
            if (char === '"' || char === "'") {
                quoteChar = char;
                break;
            }
        }

        // If we didn't find a quote by searching backward, search forward from the start
        if (!quoteChar) {
            for (let i = diagnostic.range.start.character; i < lineText.length; i += 1) {
                const char = lineText[i];
                if (char === '"' || char === "'") {
                    quoteChar = char;
                    break;
                }
            }
        }

        if (!quoteChar) {
            return null;
        }

        // Create a CodeAction to add closing quote
        const fix = new vscode.CodeAction(`Add closing ${quoteChar}`, vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();

        // Add closing quote at end of line
        const endPosition = new vscode.Position(diagnostic.range.start.line, lineText.length);
        fix.edit.insert(document.uri, endPosition, quoteChar);
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Creates a quick fix code action for unclosed interpolation.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction|null} - The code action or null
     */
    createUnclosedInterpolationFix(document, diagnostic) {
        const line = document.lineAt(diagnostic.range.start.line);
        const lineText = line.text;

        // Create a CodeAction to add closing ^
        const fix = new vscode.CodeAction("Add closing ^", vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();

        // Add closing ^ at end of line
        const endPosition = new vscode.Position(diagnostic.range.start.line, lineText.length);
        fix.edit.insert(document.uri, endPosition, "^");
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Creates quick fixes for case mismatch diagnostics.
     * Action 0: fix current occurrence to match declaration.
     * Action 1: normalize declaration and usages to current occurrence style.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction[]} - Quick fixes
     */
    createInterpolationCaseMismatchFixes(document, diagnostic) {
        let variableName = diagnostic.data?.params?.variableName;
        let declaredName = diagnostic.data?.params?.declaredName;

        if (!variableName || !declaredName) {
            const match = diagnostic.message.match(/Variable '([^']+)'\s+capitalisation differs from declared variable '([^']+)'\./i);
            if (!match) {
                return [];
            }
            variableName = match[1];
            declaredName = match[2];
        }

        const fixes = [];

        // Fix 1: update only the current mismatched occurrence to declaration casing.
        const fixCurrent = new vscode.CodeAction(
            `Change '${variableName}' to '${declaredName}'`,
            vscode.CodeActionKind.QuickFix
        );
        fixCurrent.edit = new vscode.WorkspaceEdit();
        fixCurrent.edit.replace(document.uri, diagnostic.range, declaredName);
        fixCurrent.diagnostics = [diagnostic];
        fixes.push(fixCurrent);

        // Fix 2: update declaration/usages to match current occurrence casing.
        const fixAllToCurrent = new vscode.CodeAction(
            `Normalize '${declaredName}' to '${variableName}' in file`,
            vscode.CodeActionKind.QuickFix
        );
        fixAllToCurrent.edit = new vscode.WorkspaceEdit();
        this.replaceVariableReferences(document, declaredName, variableName, fixAllToCurrent.edit);
        fixAllToCurrent.diagnostics = [diagnostic];
        fixes.push(fixAllToCurrent);

        return fixes;
    }

    /**
     * Creates quick fixes for function name case mismatch diagnostics.
     * Action 0: fix this function call to match declaration casing.
     * Action 1: normalize declaration and calls in this file to current call casing.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction[]} - Quick fixes
     */
    createFunctionCaseMismatchFixes(document, diagnostic) {
        let functionName = diagnostic.data?.params?.functionName;
        let declaredName = diagnostic.data?.params?.declaredName;

        if (!functionName || !declaredName) {
            const match = diagnostic.message.match(/Function '([^']+)' capitalisation differs from declared function '([^']+)'\./i);
            if (!match) {
                return [];
            }
            functionName = match[1];
            declaredName = match[2];
        }

        const fixes = [];

        const fixCurrent = new vscode.CodeAction(
            `Change function '${functionName}' to '${declaredName}'`,
            vscode.CodeActionKind.QuickFix
        );
        fixCurrent.edit = new vscode.WorkspaceEdit();
        fixCurrent.edit.replace(document.uri, diagnostic.range, declaredName);
        fixCurrent.diagnostics = [diagnostic];
        fixes.push(fixCurrent);

        const fixAllToCurrent = new vscode.CodeAction(
            `Normalize function '${declaredName}' to '${functionName}' in file`,
            vscode.CodeActionKind.QuickFix
        );
        fixAllToCurrent.edit = new vscode.WorkspaceEdit();
        this.replaceFunctionReferences(document, declaredName, functionName, fixAllToCurrent.edit);
        fixAllToCurrent.diagnostics = [diagnostic];
        fixes.push(fixAllToCurrent);

        return fixes;
    }

    /**
    * Creates a quick fix to remove 'h' prefix from a database field-like variable name.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction|null} - The code action or null
     */
    createRemoveDatabaseFieldPrefixFix(document, diagnostic) {
        const line = document.lineAt(diagnostic.range.start.line);
        const lineText = line.text;

        // Extract variable name from the line (VAR hName : TYPE)
        const varMatch = lineText.match(/VAR\s+([hH]([A-Za-z_][A-Za-z0-9_]*))\s*:/i);
        if (!varMatch) {
            return null;
        }

        const oldName = varMatch[1];
        const newName = varMatch[2]; // Remove the h prefix

        // Create fix suggestion
        const fix = new vscode.CodeAction(`Rename '${oldName}' to '${newName}'`, vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();

        // Replace all occurrences in the document
        this.replaceVariableReferences(document, oldName, newName, fix.edit);
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Creates a quick fix to rename variable with corrected naming prefix.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @param {string} diagnosticType - The diagnostic type (namingPrefixType or namingPrefixScope)
     * @returns {vscode.CodeAction|null} - The code action or null
     */
    createVariableRenameFix(document, diagnostic, diagnosticType) {
        // Extract the old and new name from the diagnostic message
        // Message format: "Naming convention (type/scope): 'oldName' ... Consider 'newName'."
        const messageMatch = diagnostic.message.match(/Consider '([^']+)'/);
        if (!messageMatch) {
            return null;
        }

        const newName = messageMatch[1];

        // Find the variable name from the line (it's the first word before the colon)
        const line = document.lineAt(diagnostic.range.start.line);
        const lineText = line.text;

        const varMatch = lineText.match(/VAR\s+([A-Za-z_][A-Za-z0-9_]*)\s*:/i);
        if (!varMatch) {
            return null;
        }

        const oldName = varMatch[1];

        // Create fix suggestion
        const fix = new vscode.CodeAction(`Rename '${oldName}' to '${newName}'`, vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();

        // Replace all occurrences in the document
        this.replaceVariableReferences(document, oldName, newName, fix.edit);
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Replaces all references to a variable throughout the document.
     * @param {vscode.TextDocument} document - The document
     * @param {string} oldName - The old variable name
     * @param {string} newName - The new variable name
     * @param {vscode.WorkspaceEdit} edit - The workspace edit to add replacements to
     */
    replaceVariableReferences(document, oldName, newName, edit) {
        // Create a regex to match the variable as a whole word
        const regex = new RegExp(`\\b${oldName}\\b`, "g");

        for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
            const lineText = document.lineAt(lineIndex).text;

            // Skip lines with comments or string literals for safety
            // Find all matches in this line
            let match;
            while ((match = regex.exec(lineText)) !== null) {
                // Skip if inside a quoted string
                const beforeMatch = lineText.substring(0, match.index);
                const inQuote = this.isInQuotedString(beforeMatch);
                
                if (!inQuote) {
                    const range = new vscode.Range(
                        lineIndex,
                        match.index,
                        lineIndex,
                        match.index + oldName.length
                    );
                    edit.replace(document.uri, range, newName);
                }
            }
        }
    }

    /**
     * Replaces function declaration/call references throughout the document.
     * @param {vscode.TextDocument} document - The document
     * @param {string} oldName - Existing function name
     * @param {string} newName - Replacement function name
     * @param {vscode.WorkspaceEdit} edit - Workspace edit collector
     */
    replaceFunctionReferences(document, oldName, newName, edit) {
        const declarationRegex = new RegExp(`^(\\s*#?\\s*function\\s+)${oldName}(\\s*\\()`, "i");
        const callRegex = new RegExp(`\\b${oldName}\\b\\s*\\(`, "gi");

        for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
            const lineText = document.lineAt(lineIndex).text;

            const declarationMatch = lineText.match(declarationRegex);
            if (declarationMatch) {
                const declStart = declarationMatch[1].length;
                const declEnd = declStart + oldName.length;
                edit.replace(
                    document.uri,
                    new vscode.Range(lineIndex, declStart, lineIndex, declEnd),
                    newName
                );
            }

            let callMatch;
            while ((callMatch = callRegex.exec(lineText)) !== null) {
                const callStart = callMatch.index;
                const beforeMatch = lineText.substring(0, callStart);
                if (this.isInQuotedString(beforeMatch)) {
                    continue;
                }

                edit.replace(
                    document.uri,
                    new vscode.Range(lineIndex, callStart, lineIndex, callStart + oldName.length),
                    newName
                );
            }
        }
    }

    /**
     * Checks if a position is inside a quoted string.
     * @param {string} text - The text before the position
     * @returns {boolean} - True if the position is inside a quoted string
     */
    isInQuotedString(text) {
        let inSingleQuote = false;
        let inDoubleQuote = false;

        for (let i = 0; i < text.length; i += 1) {
            const char = text[i];
            if (char === "'" && !inDoubleQuote) {
                inSingleQuote = !inSingleQuote;
            } else if (char === '"' && !inSingleQuote) {
                inDoubleQuote = !inDoubleQuote;
            }
        }

        return inSingleQuote || inDoubleQuote;
    }

    /**
     * Creates a quick fix for ACU-PAT-003: no INDEX_SAVE_POSITION in scope.
     * Scans the document for INDEX_OPEN_* to extract the table synonym, then
     * inserts INDEX_SAVE_POSITION immediately before the OPEN line.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction[]} - Quick fixes
     */
    createIndexSaveRestoreFixes(document, diagnostic) {
        const fixes = [];

        // Find the nearest INDEX_OPEN_* or SQL_INDEX_OPEN_STANDARD above the diagnostic line to extract the synonym.
        const openPattern = /(?:SQL_)?INDEX_OPEN_\w+\s*\(\s*"([^"]+)"/;
        let openLineIndex = -1;
        let synonym = null;
        for (let i = diagnostic.range.start.line; i >= 0; i--) {
            const openMatch = document.lineAt(i).text.match(openPattern);
            if (openMatch) {
                openLineIndex = i;
                synonym = openMatch[1];
                break;
            }
        }

        if (openLineIndex === -1 || synonym === null) {
            return fixes;
        }

        const openLineText = document.lineAt(openLineIndex).text;
        const indent = openLineText.match(/^\s*/)[0];

        const addSave = new vscode.CodeAction("Insert INDEX_SAVE_POSITION()", vscode.CodeActionKind.QuickFix);
        addSave.edit = new vscode.WorkspaceEdit();
        addSave.edit.insert(
            document.uri,
            new vscode.Position(openLineIndex, 0),
            `${indent}sStatus = INDEX_SAVE_POSITION("${synonym}")\n`
        );
        addSave.diagnostics = [diagnostic];
        fixes.push(addSave);

        return fixes;
    }

    /**
     * Creates a quick fix for ACU-PAT-002: INDEX_SAVE_POSITION without a matching RESTORE.
     * Extracts the table synonym from the SAVE call on the diagnostic line, then inserts
     * INDEX_RESTORE_POSITION immediately after the next INDEX_CLOSE in scope.
     * @param {vscode.TextDocument} document - The document
     * @param {vscode.Diagnostic} diagnostic - The diagnostic
     * @returns {vscode.CodeAction|null} - Quick fix, or null if context cannot be resolved
     */
    createIndexRestorePositionFix(document, diagnostic) {
        const saveLineIndex = diagnostic.range.start.line;
        const saveLineText = document.lineAt(saveLineIndex).text;

        // Extract table synonym from INDEX_SAVE_POSITION("synonym")
        const synonymMatch = saveLineText.match(/INDEX_SAVE_POSITION\s*\(\s*"([^"]+)"/);
        if (!synonymMatch) {
            return null;
        }
        const synonym = synonymMatch[1];

        // Find the first INDEX_CLOSE or SQL_INDEX_CLOSE after the SAVE line whose synonym matches
        let closeLineIndex = -1;
        for (let i = saveLineIndex + 1; i < document.lineCount; i++) {
            const closeMatch = document.lineAt(i).text.match(/\b(?:SQL_)?INDEX_CLOSE\s*\(\s*["']([^"']+)["']\s*\)/i);
            if (closeMatch && closeMatch[1] === synonym) {
                closeLineIndex = i;
                break;
            }
        }
        if (closeLineIndex === -1) {
            return null;
        }

        const closeLine = document.lineAt(closeLineIndex);
        const indent = closeLine.text.match(/^\s*/)[0];

        const fix = new vscode.CodeAction("Insert INDEX_RESTORE_POSITION()", vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();
        fix.edit.insert(
            document.uri,
            new vscode.Position(closeLineIndex, closeLine.text.length),
            `\n${indent}sStatus = INDEX_RESTORE_POSITION("${synonym}")`
        );
        fix.diagnostics = [diagnostic];

        return fix;
    }

    /**
     * Creates a quick fix for ACU-TYPE-003: replaces the INTEGER type token with LONG.
     * The diagnostic range spans exactly the INTEGER token, so the fix is a direct range replace.
     * @param {vscode.TextDocument} document
     * @param {vscode.Diagnostic} diagnostic
     * @returns {vscode.CodeAction|null}
     */
    createDiscouragedIntegerTypeFix(document, diagnostic) {
        const lineText = document.lineAt(diagnostic.range.start.line).text;
        const start = diagnostic.range.start.character;
        const end = diagnostic.range.end.character;
        if (lineText.substring(start, end).toUpperCase() !== "INTEGER") return null;

        const fix = new vscode.CodeAction("Replace 'INTEGER' with 'LONG'", vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();
        fix.edit.replace(document.uri, diagnostic.range, "LONG");
        fix.diagnostics = [diagnostic];
        return fix;
    }

    /**
     * Creates a quick fix to delete a redundant EXIT or END-OF-CODE line.
     * Handles both mid-file lines (deletes the line including its trailing newline)
     * and the last line of the file (removes the preceding newline instead).
     * @param {vscode.TextDocument} document
     * @param {vscode.Diagnostic} diagnostic
     * @returns {vscode.CodeAction|null}
     */
    createRedundantTerminatorFix(document, diagnostic) {
        const lineIndex = diagnostic.range.start.line;
        const lineText = document.lineAt(lineIndex).text;
        const keyword = /END-OF-CODE/i.test(lineText) ? "END-OF-CODE" : "EXIT";

        const fix = new vscode.CodeAction(
            `Remove redundant ${keyword} statement`,
            vscode.CodeActionKind.QuickFix
        );
        fix.edit = new vscode.WorkspaceEdit();

        if (lineIndex + 1 < document.lineCount) {
            // Not the last line: delete from start of this line to start of next
            fix.edit.replace(
                document.uri,
                new vscode.Range(lineIndex, 0, lineIndex + 1, 0),
                ""
            );
        } else if (lineIndex > 0) {
            // Last line (and not the only line): delete the preceding newline and this line's content
            const prevLineText = document.lineAt(lineIndex - 1).text;
            fix.edit.replace(
                document.uri,
                new vscode.Range(lineIndex - 1, prevLineText.length, lineIndex, document.lineAt(lineIndex).text.length),
                ""
            );
        } else {
            // Single-line document: replace the entire line content with nothing
            fix.edit.replace(
                document.uri,
                new vscode.Range(0, 0, 0, document.lineAt(0).text.length),
                ""
            );
        }

        fix.diagnostics = [diagnostic];
        return fix;
    }

    /**
     * Creates a quick fix to replace END-OF-CODE with EXIT.
     * @param {vscode.TextDocument} document
     * @param {vscode.Diagnostic} diagnostic
     * @returns {vscode.CodeAction|null}
     */
    createDeprecatedEndOfCodeFix(document, diagnostic) {
        const lineIndex = diagnostic.range.start.line;
        const lineText = document.lineAt(lineIndex).text;
        const startCol = lineText.indexOf("END-OF-CODE");
        if (startCol === -1) return null;

        const fix = new vscode.CodeAction(
            "Replace END-OF-CODE with EXIT",
            vscode.CodeActionKind.QuickFix
        );
        fix.edit = new vscode.WorkspaceEdit();
        fix.edit.replace(
            document.uri,
            new vscode.Range(lineIndex, startCol, lineIndex, startCol + "END-OF-CODE".length),
            "EXIT"
        );
        fix.diagnostics = [diagnostic];
        return fix;
    }

    /**
     * Creates a quick fix for ACU-FNC-012: removes whitespace between a function name and '('.
     * The diagnostic range covers exactly the whitespace characters, so the fix is a range deletion.
     * @param {vscode.TextDocument} document
     * @param {vscode.Diagnostic} diagnostic
     * @returns {vscode.CodeAction|null}
     */
    createFunctionWhitespaceFix(document, diagnostic) {
        const lineText = document.lineAt(diagnostic.range.start.line).text;
        const start = diagnostic.range.start.character;
        const end = diagnostic.range.end.character;

        // Verify the range contains only whitespace (guard against stale diagnostics).
        if (!/^\s+$/.test(lineText.substring(start, end))) return null;

        const fix = new vscode.CodeAction("Remove whitespace before '('", vscode.CodeActionKind.QuickFix);
        fix.edit = new vscode.WorkspaceEdit();
        fix.edit.replace(
            document.uri,
            new vscode.Range(diagnostic.range.start.line, start, diagnostic.range.start.line, end),
            ""
        );
        fix.diagnostics = [diagnostic];
        return fix;
    }

    /**
     * Creates a quick fix for ACU-VAR-007: removes an unused variable declaration.
     *
     * - Single-variable declaration line (no comma in declaration): deletes the entire line.
     * - Multi-variable declaration line: removes just this variable and its adjacent comma.
     *
     * @param {vscode.TextDocument} document
     * @param {vscode.Diagnostic} diagnostic
     * @returns {vscode.CodeAction|null}
     */
    createUnusedVariableDeclarationFix(document, diagnostic) {
        let variableName = diagnostic.data?.params?.variableName;
        if (!variableName) {
            const match = diagnostic.message.match(/'([^']+)' is declared but never used/);
            if (!match) return null;
            variableName = match[1];
        }

        const lineIndex = diagnostic.range.start.line;
        const lineText = document.lineAt(lineIndex).text;

        // Split at first '//' to separate code from comment.
        const commentIdx = lineText.indexOf("//");
        const codePart = commentIdx >= 0 ? lineText.substring(0, commentIdx) : lineText;
        const commentPart = commentIdx >= 0 ? lineText.substring(commentIdx) : "";

        const hasMultipleVars = codePart.includes(",");

        const fix = new vscode.CodeAction(
            hasMultipleVars
                ? `Remove unused variable '${variableName}' from declaration`
                : `Remove unused variable declaration '${variableName}'`,
            vscode.CodeActionKind.QuickFix
        );
        fix.edit = new vscode.WorkspaceEdit();
        fix.diagnostics = [diagnostic];

        if (!hasMultipleVars) {
            // Delete the entire line (including its trailing newline).
            if (lineIndex + 1 < document.lineCount) {
                fix.edit.replace(document.uri, new vscode.Range(lineIndex, 0, lineIndex + 1, 0), "");
            } else if (lineIndex > 0) {
                const prevLineText = document.lineAt(lineIndex - 1).text;
                fix.edit.replace(
                    document.uri,
                    new vscode.Range(lineIndex - 1, prevLineText.length, lineIndex, lineText.length),
                    ""
                );
            } else {
                fix.edit.replace(document.uri, new vscode.Range(0, 0, 0, lineText.length), "");
            }
        } else {
            // Remove only this variable name and its adjacent comma from the declaration.
            // Try "varName," first (variable is not last); fall back to ",varName" (variable is last).
            const escapedName = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
            const removeFirstRegex  = new RegExp(`\\b${escapedName}\\b\\s*,\\s*`);
            const removeNotFirstRegex = new RegExp(`\\s*,\\s*\\b${escapedName}\\b`);

            let newCode;
            if (removeFirstRegex.test(codePart)) {
                newCode = codePart.replace(removeFirstRegex, "");
            } else {
                newCode = codePart.replace(removeNotFirstRegex, "");
            }

            fix.edit.replace(
                document.uri,
                new vscode.Range(lineIndex, 0, lineIndex, lineText.length),
                newCode + commentPart
            );
        }

        return fix;
    }

    /**
     * Creates a "Suppress this line" quick fix that inserts a `// @rule-ignore ACU-XXX-NNN`
     * comment on the line above the diagnostic, with a REASON snippet placeholder.
     *
     * Returns null if the diagnostic has no messageId or is marked non-suppressable.
     *
     * @param {vscode.TextDocument} document
     * @param {vscode.Diagnostic} diagnostic
     * @returns {vscode.CodeAction|null}
     */
    createSuppressionCommentFix(document, diagnostic) {
        const messageId = diagnostic.code;
        if (!messageId || !isSuppressable(messageId)) {
            return null;
        }

        const action = new vscode.CodeAction(
            `Suppress ${messageId} on this line`,
            vscode.CodeActionKind.QuickFix
        );
        action.command = {
            command: SUPPRESS_COMMAND_ID,
            title: "Suppress diagnostic",
            arguments: [document.uri, diagnostic.range.start.line, messageId]
        };
        action.diagnostics = [diagnostic];

        return action;
    }
}

export {
    registerCodeActions,
    KeywordCapitalizationCodeActionProvider
};
