/**
 * Function call validator for Acurity Architect.
 * Validates built-in function signatures, user-defined functions, and scope.
 */

import { isBuiltInGlobalVariable } from "../builtins/functions.js";
import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { stripTrailingComment, stripQuotedStrings, parseFunctionDeclaration } from "../parser.js";
import { NodeType } from "../ast/nodes.js";
import { buildDocumentAst } from "../ast/documentAst.js";
import { walkStatements } from "../ast/astHelpers.js";
import { parseExpression } from "../ast/expressionParser.js";
import { ExprNodeType } from "../ast/expressionNodes.js";
import { ACURITY_KEYWORDS } from "../constants.js";
import { buildIncludedSymbolsInScope } from "../includeResolver.js";
import { findBuiltinCollisionDeclarations } from "../analysis/functionAnalysis.js";
import {
    resolveKnownFunction,
    validateKnownFunctionArgumentCount
} from "../analysis/functionResolution.js";
import { extractOutputDelimiters, getFunctionValidationTargetsForLine } from "../analysis/executionContext.js";
import { extractFunctionArgs, countArguments } from "./validatorHelpers.js";
import { validateOutputDirective } from "./outputDirectiveValidator.js";


function isKnownCallableFunctionAtLine(functionName, lineIndex, userDefinedFunctionCatalog, includedFunctionsByLine) {
    const upperName = functionName.toUpperCase();
    const knownFunction = resolveKnownFunction(functionName, userDefinedFunctionCatalog);

    if (knownFunction?.kind === "builtin") {
        return true;
    }

    const userFunctionMetadata = userDefinedFunctionCatalog[upperName];
    if (userFunctionMetadata && userFunctionMetadata.lineIndex < lineIndex) {
        return true;
    }

    const includedFunctions = includedFunctionsByLine.get(lineIndex) || new Set();
    if (includedFunctions.has(upperName)) {
        return true;
    }

    return false;
}

/**
 * Validates function calls in a document.
 * @param {vscode.TextDocument} document - The document to validate
 * @returns {vscode.Diagnostic[]} - Array of function call diagnostics
 */
function validateFunctionCalls(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const outputDelimiters = options.outputDelimiters || extractOutputDelimiters(document);
    
    const userDefinedFunctionCatalog = options.userDefinedFunctionCatalog || {};
    const userDefinedFunctionCaseMap = options.userDefinedFunctionCaseMap || {};
    const functionDuplicates = options.functionDuplicates || [];
    const builtinCollisions = options.builtinCollisions || findBuiltinCollisionDeclarations(document);
    let includedFunctionsByLine = options.includedFunctionsByLine;
    let includedFunctionCasesByLine = options.includedFunctionCaseMapByLine;
    if (!includedFunctionsByLine || !includedFunctionCasesByLine) {
        const symbolScope = buildIncludedSymbolsInScope(document);
        includedFunctionsByLine = includedFunctionsByLine || symbolScope.includedFunctionsByLine;
        includedFunctionCasesByLine = includedFunctionCasesByLine || symbolScope.includedFunctionCaseMapByLine;
    }
    const macroCatalog = options.macroCatalog || null;

    let nodeTypeByLine = options.nodeTypeByLine || null;
    let stmtByLine = options.stmtByLine || null;
    if (!nodeTypeByLine || !stmtByLine) {
        const { ast } = buildDocumentAst(document);
        const _ntbl = new Map();
        const _sbl = new Map();
        walkStatements(ast.statements, stmt => {
            if (typeof stmt.line === "number") {
                _ntbl.set(stmt.line, stmt.type);
                _sbl.set(stmt.line, stmt);
            }
        });
        nodeTypeByLine = nodeTypeByLine || _ntbl;
        stmtByLine = stmtByLine || _sbl;
    }

    // Report builtin name collision declarations
    for (const collision of builtinCollisions) {
        if (collision.isCollision) {
            const lineText = document.lineAt(collision.lineIndex).text;
            const lowerLineText = lineText.toLowerCase();
            const startPos = lowerLineText.indexOf("function");
            if (startPos === -1) continue;
            const endPos = lineText.indexOf("(", startPos);
            if (endPos === -1) continue;
            const colDiag = createDiagnostic(
                { start: { line: collision.lineIndex, character: startPos }, end: { line: collision.lineIndex, character: endPos } },
                { messageId: "ACU-FNC-013", params: { functionName: collision.functionName } },
                getRuleSeverity
            );
            if (colDiag) {
                diagnostics.push(colDiag);
            }
        }
    }

    // Report duplicate function declarations
    for (const duplicate of functionDuplicates) {
        if (duplicate.isDuplicate) {
            const lineText = document.lineAt(duplicate.lineIndex).text;
            const lowerLineText = lineText.toLowerCase();
            const startPos = lowerLineText.indexOf("function");
            
            // Validate position before creating Range
            if (startPos === -1) {
                continue;  // function keyword not found, skip
            }
            
            const endPos = lineText.indexOf("(", startPos);
            if (endPos === -1) {
                continue;  // opening parenthesis not found, skip
            }
            
            const dupDiag = createDiagnostic(
                { start: { line: duplicate.lineIndex, character: startPos }, end: { line: duplicate.lineIndex, character: endPos } },
                { messageId: "ACU-FNC-003", params: { functionName: duplicate.functionName } },
                getRuleSeverity
            );
            if (dupDiag) {
                diagnostics.push(dupDiag);
            }
        }
    }

    // Main pass: validate function calls
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const rawLineText = document.lineAt(lineIndex).text;
        const lineText = stripTrailingComment(rawLineText);
        const isOutputLine = options.executionClassificationMap?.[lineIndex]?.isOutput === true;

        const lineNodeType = nodeTypeByLine.get(lineIndex);
        if (lineNodeType === NodeType.FunctionDeclaration ||
            lineNodeType === NodeType.MacroDeclaration ||
            lineNodeType === NodeType.IncludeDirective ||
            lineNodeType === NodeType.RequireDirective ||
            lineNodeType === NodeType.CommentLine) {
            continue;
        }

        if (!isOutputLine && lineNodeType === NodeType.ExpressionStatement) {
            const stmt = stmtByLine.get(lineIndex);
            const expr = stmt?.value ? parseExpression(stmt.value) : null;
            if (expr?.type === ExprNodeType.CALL &&
                isKnownCallableFunctionAtLine(expr.callee.name, lineIndex, userDefinedFunctionCatalog, includedFunctionsByLine)) {
                const standaloneCallName = expr.callee.name;
                const functionChar = expr.callee.token?.char ?? rawLineText.toUpperCase().indexOf(standaloneCallName.toUpperCase());
                const range = {
                    start: { line: lineIndex, character: functionChar },
                    end:   { line: lineIndex, character: functionChar + standaloneCallName.length }
                };
                const unusedResultDiagnostic = createDiagnostic(
                    range,
                    { messageId: "ACU-FNC-008", params: { functionName: standaloneCallName } },
                    getRuleSeverity
                );
                if (unusedResultDiagnostic) {
                    diagnostics.push(unusedResultDiagnostic);
                }
            }
        }

        const validationTargets = options.functionValidationTargetsByLine?.[lineIndex]
            || getFunctionValidationTargetsForLine(lineText, outputDelimiters);
        for (const { snippet, position } of validationTargets) {
            validateCodeSnippet(
                snippet,
                lineIndex,
                position,
                diagnostics,
                userDefinedFunctionCatalog,
                userDefinedFunctionCaseMap,
                includedFunctionsByLine,
                includedFunctionCasesByLine,
                getRuleSeverity,
                document,
                { isFromOutputLine: isOutputLine, macroCatalog }
            );
        }
    }

    return diagnostics;
}

function createFunctionNameRange(lineIndex, actualCharPos, functionName) {
    return { start: { line: lineIndex, character: actualCharPos }, end: { line: lineIndex, character: actualCharPos + functionName.length } };
}

function addFunctionDiagnostic(diagnostics, range, messageId, params, getRuleSeverity) {
    const diag = createDiagnostic(range, { messageId, params }, getRuleSeverity);
    if (diag) {
        diagnostics.push(diag);
    }
}

function validateNonBuiltinFunctionContext(
    functionName,
    upperFunctionName,
    lineIndex,
    actualCharPos,
    diagnostics,
    userDefinedFunctionCatalog,
    userDefinedFunctionCaseMap,
    includedFunctionsByLine,
    includedFunctionCasesByLine,
    getRuleSeverity
) {
    const userFunctionMetadata = userDefinedFunctionCatalog[upperFunctionName];
    const localDefinitionLine = userFunctionMetadata?.lineIndex;
    const isUserDefined = localDefinitionLine !== undefined;
    const isLocallyInScope = isUserDefined && localDefinitionLine < lineIndex;

    const includedFunctions = includedFunctionsByLine.get(lineIndex) || new Set();
    const isIncludedInScope = includedFunctions.has(upperFunctionName);

    if (!isUserDefined && !isIncludedInScope) {
        addFunctionDiagnostic(
            diagnostics,
            createFunctionNameRange(lineIndex, actualCharPos, functionName),
            "ACU-FNC-001",
            { functionName },
            getRuleSeverity
        );
        return { handled: true, isLocallyInScope: false, userFunctionMetadata: null };
    }

    if (isUserDefined && !isLocallyInScope) {
        addFunctionDiagnostic(
            diagnostics,
            createFunctionNameRange(lineIndex, actualCharPos, functionName),
            "ACU-FNC-002",
            { functionName },
            getRuleSeverity
        );
        return { handled: true, isLocallyInScope, userFunctionMetadata };
    }

    if (isUserDefined) {
        const declaredName = userDefinedFunctionCaseMap[upperFunctionName] || functionName;
        if (declaredName !== functionName) {
            addFunctionDiagnostic(
                diagnostics,
                createFunctionNameRange(lineIndex, actualCharPos, functionName),
                "ACU-FNC-007",
                { functionName, declaredName },
                getRuleSeverity
            );
        }
    } else if (isIncludedInScope) {
        const includedCaseMap = includedFunctionCasesByLine.get(lineIndex) || new Map();
        const declaredName = includedCaseMap.get(upperFunctionName) || functionName;
        if (declaredName !== functionName) {
            addFunctionDiagnostic(
                diagnostics,
                createFunctionNameRange(lineIndex, actualCharPos, functionName),
                "ACU-FNC-007",
                { functionName, declaredName },
                getRuleSeverity
            );
        }
    }

    return { handled: false, isLocallyInScope, userFunctionMetadata };
}

function validateFunctionArgumentShape(
    code,
    match,
    lineIndex,
    actualCharPos,
    rawLineText,
    functionName,
    diagnostics,
    userDefinedFunctionCatalog,
    getRuleSeverity
) {
    const startParen = match.index + match[0].length - 1;
    const argsText = extractFunctionArgs(code, startParen);

    if (argsText === null) {
        // A continuation marker at line end means this is a deliberate multi-line
        // function call — the closing paren is on a following line. Not an error.
        if (/\.\.\.\s*$/.test(code)) {
            return;
        }
        addFunctionDiagnostic(
            diagnostics,
            { start: { line: lineIndex, character: actualCharPos }, end: { line: lineIndex, character: rawLineText.length } },
            "ACU-FNC-005",
            { functionName },
            getRuleSeverity
        );
        return;
    }

    const argCount = countArguments(argsText);
    const argumentCountError = validateKnownFunctionArgumentCount(functionName, argCount, userDefinedFunctionCatalog);
    if (argumentCountError) {
        addFunctionDiagnostic(
            diagnostics,
            { start: { line: lineIndex, character: actualCharPos }, end: { line: lineIndex, character: actualCharPos + match[0].length } },
            "ACU-FNC-006",
            { errorDetail: argumentCountError },
            getRuleSeverity
        );
    }
}

/**
 * Validates code snippet and adds diagnostics
 * @param {string} code - The code to validate
 * @param {number} lineIndex - The line index
 * @param {number} codePositionInLine - The starting character position of this code in the original line (for extracted snippets)
 * @param {vscode.Diagnostic[]} diagnostics - Diagnostics array to append to
 * @param {Object} userDefinedFunctions - User function map
 * @param {Map} includedFunctionsByLine - Included functions map
 * @param {Function} getRuleSeverity - Function to get severity
 * @param {vscode.TextDocument} document - The document
 */
function validateCodeSnippet(code, lineIndex, codePositionInLine, diagnostics, userDefinedFunctionCatalog, userDefinedFunctionCaseMap, includedFunctionsByLine, includedFunctionCasesByLine, getRuleSeverity, document, snippetOptions = {}) {
    const { isFromOutputLine = false, macroCatalog = null } = snippetOptions;
    const rawLineText = document.lineAt(lineIndex).text;
    // Scan a quote-masked mirror so identifiers inside string literals are ignored
    // while preserving character offsets for accurate ranges.
    const scanCode = stripQuotedStrings(code);
    const functionPattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/gi;
    let match;

    while ((match = functionPattern.exec(scanCode)) !== null) {
        const functionName = match[1];
        const upperFunctionName = functionName.toUpperCase();
        
        // Calculate actual position in the original line
        const actualCharPos = codePositionInLine + match.index;
        
        // Validate position is within bounds
        if (actualCharPos < 0 || actualCharPos >= rawLineText.length) {
            continue;
        }
        
        // Skip keywords (e.g., WHILE in "DO WHILE (condition)")
        if (ACURITY_KEYWORDS.has(upperFunctionName)) {
            continue;
        }
        
        // Skip macro invocations — the macro validator handles those independently
        if (macroCatalog && macroCatalog[upperFunctionName]) {
            continue;
        }
        
        // Check if it's a built-in constant used incorrectly with parentheses
        if (isBuiltInGlobalVariable(functionName)) {
            addFunctionDiagnostic(
                diagnostics,
                createFunctionNameRange(lineIndex, actualCharPos, functionName),
                "ACU-FNC-004",
                { functionName },
                getRuleSeverity
            );
            continue;
        }
        
        const knownFunction = resolveKnownFunction(functionName, userDefinedFunctionCatalog);

        // Output-positioning directives (CENTER, COL, FROM, TAB, TO) — delegate
        // all context and shape validation to the dedicated validator.
        if (knownFunction && knownFunction.kind === "output-directive") {
            validateOutputDirective(
                code, match, lineIndex, actualCharPos,
                functionName, knownFunction, isFromOutputLine,
                diagnostics, getRuleSeverity
            );
            continue;
        }

        const builtInSignature = knownFunction && knownFunction.kind === "builtin";

        // If not a built-in function, validate existence/scope and case mismatches.
        if (!builtInSignature) {
            const scopeResult = validateNonBuiltinFunctionContext(
                functionName,
                upperFunctionName,
                lineIndex,
                actualCharPos,
                diagnostics,
                userDefinedFunctionCatalog,
                userDefinedFunctionCaseMap,
                includedFunctionsByLine,
                includedFunctionCasesByLine,
                getRuleSeverity
            );
            if (scopeResult.handled) {
                continue;
            }

            // User-defined function argument count checking when in scope.
            if (scopeResult.isLocallyInScope && scopeResult.userFunctionMetadata) {
                validateFunctionArgumentShape(
                    code,
                    match,
                    lineIndex,
                    actualCharPos,
                    rawLineText,
                    functionName,
                    diagnostics,
                    userDefinedFunctionCatalog,
                    getRuleSeverity
                );
            }

            continue;
        }

        // Built-in case — warn if deprecated.
        if (knownFunction.signature.deprecated) {
            addFunctionDiagnostic(
                diagnostics,
                createFunctionNameRange(lineIndex, actualCharPos, functionName),
                "ACU-FNC-016",
                { functionName },
                getRuleSeverity
            );
        }

        // Built-in case — validate argument shape.
        validateFunctionArgumentShape(
            code,
            match,
            lineIndex,
            actualCharPos,
            rawLineText,
            functionName,
            diagnostics,
            userDefinedFunctionCatalog,
            getRuleSeverity
        );
    }
}

export {
    validateFunctionCalls,
    validateFunctionDeclarations
};

/**
 * Validates user-defined function declaration syntax.
 * Emits ACU-FNC-014 for parameters missing a type annotation.
 * Emits ACU-FNC-015 for function declarations missing a return type.
 * @param {object} document - VS Code TextDocument or PlainTextDocument
 * @param {Function} getRuleSeverity
 * @returns {object[]}
 */
function validateFunctionDeclarations(document, getRuleSeverity) {
    const diagnostics = [];

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const rawLineText = document.lineAt(lineIndex).text;
        const decl = parseFunctionDeclaration(rawLineText);
        if (!decl) {
            continue;
        }

        const effectiveText = stripTrailingComment(rawLineText);

        // ACU-FNC-014: one diagnostic per parameter missing a type annotation
        for (const malformed of decl.malformedParams) {
            const openParen = rawLineText.indexOf("(");
            const postParenIndex = openParen >= 0
                ? rawLineText.substring(openParen + 1).toLowerCase().indexOf(malformed.name.toLowerCase())
                : -1;
            let charStart;
            if (postParenIndex >= 0) {
                charStart = openParen + 1 + postParenIndex;
            } else {
                const globalIndex = rawLineText.toLowerCase().indexOf(malformed.name.toLowerCase());
                charStart = globalIndex >= 0 ? globalIndex : 0;
            }
            const range = {
                start: { line: lineIndex, character: charStart },
                end:   { line: lineIndex, character: charStart + malformed.name.length }
            };
            const diag = createDiagnostic(
                range,
                { messageId: "ACU-FNC-014", params: { paramName: malformed.name, functionName: decl.name } },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
        }

        // ACU-FNC-015: return type is absent
        if (decl.returnType === null) {
            const closeParen = effectiveText.lastIndexOf(")");
            const charStart = closeParen >= 0 ? closeParen : 0;
            const charEnd = closeParen >= 0 ? closeParen + 1 : rawLineText.trimEnd().length;
            const range = {
                start: { line: lineIndex, character: charStart },
                end:   { line: lineIndex, character: charEnd }
            };
            const diag = createDiagnostic(
                range,
                { messageId: "ACU-FNC-015", params: { functionName: decl.name } },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
        }
    }

    return diagnostics;
}
