/**
 * Variable usage validator for Acurity Architect.
 *
 * Validates variable references in assignment expressions:
 * - ACU-VAR-001: Undefined variable
 * - ACU-VAR-002: Variable used before being assigned (unset)
 * - ACU-VAR-003: Unclosed string literal
 * - ACU-VAR-004: Pseudo-constant assigned more than once
 * - ACU-VAR-005: Near-miss built-in constant name (Did you mean X?)
 * - ACU-VAR-007: Variable declared but never used
 * - ACU-INT-002: Variable name used with incorrect capitalisation
 * - ACU-VAR-008: Unknown database field handle
 * - ACU-VAR-009: Database field handle used with incorrect capitalisation
 *
 * This module was extracted from the monolithic collectDiagnostics() function
 * to improve testability and separate concerns.
 */

import { parseAssignment, findUnclosedString, stripQuotedStrings, extractVariables, buildTokenRange } from "../parser.js";
import { getFunctionScopeAtLine } from "../analysis/functionAnalysis.js";
import { isBuiltInGlobalVariable, BuiltInGlobalVariables, BuiltInFunctions } from "../builtins/functions.js";
import { isDatabaseHandle } from "../constants.js";
import { getFieldEntry } from "../builtins/fieldRegistry.js";
import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { parseExpression } from "../ast/expressionParser.js";
import { ExprNodeType } from "../ast/expressionNodes.js";

// Matches identifiers that look like field handles regardless of casing:
// h + 2 letters (table code) + 1 letter (type prefix) + underscore
const FIELD_HANDLE_LIKE = /^h[A-Za-z]{2}[A-Za-z]_/;

// Maps common near-miss constant names (upper-cased) to the correct built-in constant name.
const CONSTANT_SUGGESTIONS = {
    MIN_STRING:      "MIN_STR",
    MAX_STRING:      "MAX_STR",
    NEW_LINE_STRING: "NEW_LINE_STR",
    TAB_STRING:      "TAB_STR",
};

// ---------------------------------------------------------------------------
// Scope helpers
// ---------------------------------------------------------------------------

/**
 * Checks if a variable is accessible at a given line (exact case-sensitive match).
 */
function isVariableInScope(variableName, lineIndex, globalVariables, functionScopes, includedVariablesByLine) {
    if (globalVariables.has(variableName)) {
        return true;
    }

    const includedVariables = includedVariablesByLine.get(lineIndex) || new Map();
    if (includedVariables.has(variableName)) {
        return true;
    }

    const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
    if (!scope) {
        return false;
    }

    return scope.params.has(variableName) || scope.locals.has(variableName);
}

/**
 * Finds the correctly-cased declared name for a variable referenced with wrong casing.
 * Returns null if no match exists.
 */
function findCaseInsensitiveVariableInScope(variableName, lineIndex, globalVariables, functionScopes, includedVariablesByLine) {
    const targetLower = variableName.toLowerCase();

    for (const declaredName of globalVariables.keys()) {
        if (declaredName.toLowerCase() === targetLower) {
            return declaredName;
        }
    }

    const includedVariables = includedVariablesByLine.get(lineIndex) || new Map();
    for (const declaredName of includedVariables.keys()) {
        if (declaredName.toLowerCase() === targetLower) {
            return declaredName;
        }
    }

    const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
    if (!scope) {
        return null;
    }

    for (const declaredName of scope.params.keys()) {
        if (declaredName.toLowerCase() === targetLower) {
            return declaredName;
        }
    }

    for (const declaredName of scope.locals.keys()) {
        if (declaredName.toLowerCase() === targetLower) {
            return declaredName;
        }
    }

    return null;
}

/**
 * Returns true if the variable is a function parameter (so unset warnings are suppressed).
 */
function isFunctionParameter(variableName, lineIndex, functionScopes) {
    const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
    if (!scope) return false;
    const upper = variableName.toUpperCase();
    for (const key of scope.params.keys()) {
        if (key.toUpperCase() === upper) return true;
    }
    return false;
}

/**
 * Creates a unique key for tracking initialisation state across scopes.
 */
function getScopedInitKey(variableName, lineIndex, functionScopes) {
    const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
    return scope ? `func${scope.id}:${variableName}` : `global:${variableName}`;
}

/**
 * Returns true if the variable should be treated as a pseudo-constant.
 * Pseudo-constants are ALLCAPS variables (after any single lowercase prefix) that contain "CONST".
 */
function isPseudoConstant(variableName) {
    if (!variableName || variableName.length < 2) {
        return false;
    }

    if (!variableName.toUpperCase().includes("CONST")) {
        return false;
    }

    const nameToCheck = /^[a-z]/.test(variableName) ? variableName.substring(1) : variableName;
    return /^[A-Z_0-9]+$/.test(nameToCheck);
}

// ---------------------------------------------------------------------------
// Param-mode helpers
// ---------------------------------------------------------------------------


/**
 * Inspects a function call expression and returns the set of variable names at
 * VAR_init_not_required positions (skip unset check; mark init'd after call).
 *
 * VAR_destroy tracking (stale handle detection) is handled by indexUsageValidator.js
 * using argument parsing directly — it does not belong in the unset-variable pass.
 *
 * Returns an empty set if the expression is not a recognised built-in call with paramModes.
 * Only bare variable names (Identifier nodes, no complex sub-expressions) at the relevant
 * positions are captured.
 *
 * @param {object|null} valueSpan - TokenSpan from the AST assignment node's value
 * @returns {Set<string>}
 */
function getParamModeVariables(valueSpan) {
    if (!valueSpan) return new Set();

    const expr = parseExpression(valueSpan);
    if (!expr || expr.type !== ExprNodeType.CALL) return new Set();

    const funcName = expr.callee?.name;
    if (!funcName) return new Set();

    const entry = BuiltInFunctions[funcName.toUpperCase()];
    if (!entry || !entry.paramModes) return new Set();

    const outVars = new Set();
    for (let i = 0; i < entry.paramModes.length; i++) {
        if (entry.paramModes[i] !== "VAR_init_not_required") continue;
        const arg = expr.args[i];
        if (arg && arg.type === ExprNodeType.IDENTIFIER) {
            outVars.add(arg.name);
        }
    }
    return outVars;
}

// ---------------------------------------------------------------------------
// Main validator
// ---------------------------------------------------------------------------

/**
 * Validates variable usage in assignment expressions across the document.
 *
 * @param {vscode.TextDocument} document
 * @param {Function} getRuleSeverity
 * @param {object} [options={}]
 * @param {object} [options.facts] - Pre-computed document facts from collectDocumentFacts()
 * @param {Map<string, object>} [options.globalVariables] - Global-scope declared variables
 * @returns {vscode.Diagnostic[]}
 */
function validateVariableUsage(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const { facts, globalVariables = new Map() } = options;
    if (!facts) return diagnostics;
    const { functionScopes, executionClassificationMap, includedVariablesByLine, includedInitialisedVariablesByLine, macroCatalog, interpolationTargetsByLine, doIteratorsByLine, assignmentsByLine, declaredVariables = new Map() } = facts;
    const initializedVariables = new Set();
    const pseudoConstantAssignments = new Map();

    // ACU-VAR-007: track which declared variables are referenced at least once.
    const usedDeclaredVarKeys = new Set();
    function markUsed(variableName, lineIndex) {
        const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
        const key = scope ? `${scope.startLine}:${variableName}` : `global:${variableName}`;
        if (declaredVariables.has(key)) {
            usedDeclaredVarKeys.add(key);
        }
    }

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const usageCls = executionClassificationMap[lineIndex];

        // On output lines, check interpolation targets for uninitialised variables.
        if (usageCls.isOutput) {
            const targets = interpolationTargetsByLine?.[lineIndex];
            if (targets) {
                const includedInitVarsAtLine = includedInitialisedVariablesByLine?.get(lineIndex) || new Set();
                for (const target of targets) {
                    if (target.isFunctionLike) {
                        continue;
                    }
                    const varName = target.variableName;
                    if (!isVariableInScope(varName, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
                        continue;
                    }
                    markUsed(varName, lineIndex);
                    if (isFunctionParameter(varName, lineIndex, functionScopes)) {
                        continue;
                    }
                    const initKey = getScopedInitKey(varName, lineIndex, functionScopes);
                    if (!initializedVariables.has(initKey) && !includedInitVarsAtLine.has(varName)) {
                        const diag = createDiagnostic(
                            { start: { line: lineIndex, character: target.startPos }, end: { line: lineIndex, character: target.endPos } },
                            { messageId: "ACU-VAR-002", params: { variableName: varName } },
                            getRuleSeverity
                        );
                        if (diag) diagnostics.push(diag);
                    }
                }
            }
            continue;
        }

        if (!usageCls.isExecutable) {
            continue;
        }

        const lineText = document.lineAt(lineIndex).text;

        // Resolve the assignment for this line. The AST-based assignmentsByLine is preferred;
        // when not provided in facts (e.g. minimal-facts unit tests), fall back to parseAssignment.
        let assignTarget, valueSpan, expression, expressionStart;
        if (assignmentsByLine !== undefined) {
            const astAssignment = assignmentsByLine.get(lineIndex);
            if (!astAssignment) {
                const iterVar = doIteratorsByLine?.get(lineIndex);
                if (iterVar && isVariableInScope(iterVar, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
                    initializedVariables.add(getScopedInitKey(iterVar, lineIndex, functionScopes));
                    markUsed(iterVar, lineIndex);
                }
                continue;
            }
            assignTarget = astAssignment.target;
            valueSpan = astAssignment.valueSpan;
            expression = valueSpan ? valueSpan.tokens.map(t => t.value).join("") : "";
            expressionStart = valueSpan?.start.char ?? 0;
        } else {
            // Fallback path — assignmentsByLine not provided in facts
            const legacyAssignment = parseAssignment(lineText);
            if (!legacyAssignment) {
                const iterVar = doIteratorsByLine?.get(lineIndex);
                if (iterVar && isVariableInScope(iterVar, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
                    initializedVariables.add(getScopedInitKey(iterVar, lineIndex, functionScopes));
                    markUsed(iterVar, lineIndex);
                }
                continue;
            }
            assignTarget = legacyAssignment.target;
            valueSpan = null;
            expression = legacyAssignment.expression;
            expressionStart = legacyAssignment.expressionStart;
        }

        // --- Validate assignment target ---
        if (isBuiltInGlobalVariable(assignTarget)) {
            const upper = assignTarget.toUpperCase();
            if (BuiltInGlobalVariables[upper]?.readOnly) {
                const diag = createDiagnostic(
                    buildTokenRange(lineText, lineIndex, assignTarget, 0),
                    { messageId: "ACU-VAR-006", params: { variableName: assignTarget } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            }
            continue;
        }
        if (!isDatabaseHandle(assignTarget) && !isVariableInScope(assignTarget, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
            const caseMatch = findCaseInsensitiveVariableInScope(assignTarget, lineIndex, globalVariables, functionScopes, includedVariablesByLine);
            if (caseMatch) {
                const diag = createDiagnostic(
                    buildTokenRange(lineText, lineIndex, assignTarget, 0),
                    { messageId: "ACU-INT-002", params: { variableName: assignTarget, declaredName: caseMatch } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
                initializedVariables.add(getScopedInitKey(caseMatch, lineIndex, functionScopes));
                markUsed(caseMatch, lineIndex);
                continue;
            }

            if (macroCatalog[assignTarget.toUpperCase()]) {
                continue;
            }

            const diag = createDiagnostic(
                buildTokenRange(lineText, lineIndex, assignTarget, 0),
                { messageId: "ACU-VAR-001", params: { variableName: assignTarget } },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
            continue;
        }

        // Mark the assignment target as used (it is in scope at this point).
        markUsed(assignTarget, lineIndex);

        // --- Validate expression: unclosed strings ---
        const unclosedString = findUnclosedString(expression);
        if (unclosedString) {
            const diag = createDiagnostic(
                { start: { line: lineIndex, character: expressionStart + unclosedString.start }, end: { line: lineIndex, character: lineText.length } },
                { messageId: "ACU-VAR-003", params: {} },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
        }

        // --- Inspect param modes for VAR_init_not_required args ---
        const outVars = getParamModeVariables(valueSpan);

        // --- Validate expression: variable references ---
        const expressionWithoutStrings = stripQuotedStrings(expression);
        const referencedVariables = extractVariables(expressionWithoutStrings);

        for (const referencedVariable of referencedVariables) {
            if (isBuiltInGlobalVariable(referencedVariable)) {
                continue;
            }

            if (isDatabaseHandle(referencedVariable)) {
                const fieldEntry = getFieldEntry(referencedVariable);
                if (!fieldEntry) {
                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                        { messageId: "ACU-VAR-008", params: { name: referencedVariable } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                } else if (fieldEntry.canonical !== referencedVariable) {
                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                        { messageId: "ACU-VAR-009", params: { name: referencedVariable, canonical: fieldEntry.canonical } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
                continue;
            }

            if (FIELD_HANDLE_LIKE.test(referencedVariable)) {
                const fieldEntry = getFieldEntry(referencedVariable);
                if (fieldEntry) {
                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                        { messageId: "ACU-VAR-009", params: { name: referencedVariable, canonical: fieldEntry.canonical } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                } else {
                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                        { messageId: "ACU-VAR-008", params: { name: referencedVariable } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
                continue;
            }

            if (!isVariableInScope(referencedVariable, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
                const caseMatch = findCaseInsensitiveVariableInScope(referencedVariable, lineIndex, globalVariables, functionScopes, includedVariablesByLine);
                if (caseMatch) {
                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                        { messageId: "ACU-INT-002", params: { variableName: referencedVariable, declaredName: caseMatch } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                    markUsed(caseMatch, lineIndex);
                    continue;
                }

                if (macroCatalog[referencedVariable.toUpperCase()]) {
                    continue;
                }

                const suggestion = CONSTANT_SUGGESTIONS[referencedVariable.toUpperCase()];
                if (suggestion) {
                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                        { messageId: "ACU-VAR-005", params: { variableName: referencedVariable, suggestion } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                    continue;
                }

                const diag = createDiagnostic(
                    buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                    { messageId: "ACU-VAR-001", params: { variableName: referencedVariable } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
                continue;
            }

            // Variable is in scope — mark it as used.
            markUsed(referencedVariable, lineIndex);

            // Unset variable check
            if (isFunctionParameter(referencedVariable, lineIndex, functionScopes)) {
                continue;
            }

            // VAR_init_not_required: the function will initialise this variable — no warning.
            if (outVars.has(referencedVariable)) {
                continue;
            }

            const initKey = getScopedInitKey(referencedVariable, lineIndex, functionScopes);
            const includedInitVars = includedInitialisedVariablesByLine?.get(lineIndex) || new Set();
            if (!initializedVariables.has(initKey) && !includedInitVars.has(referencedVariable)) {
                const diag = createDiagnostic(
                    buildTokenRange(lineText, lineIndex, referencedVariable, expressionStart),
                    { messageId: "ACU-VAR-002", params: { variableName: referencedVariable } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            }
        }

        // --- Mark VAR_init_not_required args as initialised after the call ---
        for (const v of outVars) {
            if (isVariableInScope(v, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
                initializedVariables.add(getScopedInitKey(v, lineIndex, functionScopes));
                markUsed(v, lineIndex);
            }
        }

        // --- Mark target as initialised; check pseudo-constant re-assignment ---
        if (isVariableInScope(assignTarget, lineIndex, globalVariables, functionScopes, includedVariablesByLine)) {
            initializedVariables.add(getScopedInitKey(assignTarget, lineIndex, functionScopes));

            if (isPseudoConstant(assignTarget)) {
                if (!pseudoConstantAssignments.has(assignTarget)) {
                    pseudoConstantAssignments.set(assignTarget, [lineIndex]);
                } else {
                    const previousAssignments = pseudoConstantAssignments.get(assignTarget);
                    previousAssignments.push(lineIndex);

                    const diag = createDiagnostic(
                        buildTokenRange(lineText, lineIndex, assignTarget, 0),
                        { messageId: "ACU-VAR-004", params: { variableName: assignTarget, firstLine: previousAssignments[0] + 1 } },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
            }
        }
    }

    // ACU-VAR-007: emit for every declared variable that was never referenced.
    for (const [key, entry] of declaredVariables) {
        if (entry.isFunctionParameter) continue;
        if (usedDeclaredVarKeys.has(key)) continue;
        const declLineText = document.lineAt(entry.line).text;
        const diag = createDiagnostic(
            buildTokenRange(declLineText, entry.line, entry.name, 0),
            { messageId: "ACU-VAR-007", params: { variableName: entry.name } },
            getRuleSeverity
        );
        if (diag) diagnostics.push(diag);
    }

    return diagnostics;
}

export {
    validateVariableUsage,
    // Exported for testing
    isVariableInScope,
    findCaseInsensitiveVariableInScope,
    isFunctionParameter,
    getScopedInitKey,
    isPseudoConstant
};
