/**
 * Argument type validator for Acurity Architect.
 * Checks that arguments passed to built-in functions match the declared
 * parameter types in the function registry.
 *
 * ACU-FNC-009 — functionArgumentType
 *   Fires when a known argument type does not satisfy the declared parameter
 *   type for a built-in function.
 *
 * Only fires when:
 *   - The function has typed params in the registry
 *   - The argument's type can be inferred (null → skip to avoid false positives)
 *   - The inferred type is not compatible with the declared parameter type
 */

import { NodeType } from "../ast/nodes.js";
import { ExprNodeType } from "../ast/expressionNodes.js";
import { buildDocumentAst } from "../ast/documentAst.js";
import { parseExpression } from "../ast/expressionParser.js";
import { inferExpressionType } from "../analysis/expressionTypeInference.js";
import { areTypesCompatible } from "../types/acurityTypes.js";
import { BuiltInFunctions } from "../builtins/functionsRegistry.js";
import { getFunctionScopeAtLine } from "../analysis/functionAnalysis.js";
import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { buildVariablesMap } from "./validatorHelpers.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Returns true if `inferredType` satisfies `paramType`.
 * paramType is either a string or an array of strings (union).
 * Uses areTypesCompatible so that numeric widening is treated as compatible.
 */
function paramTypeMatches(inferredType, paramType) {
    if (Array.isArray(paramType)) {
        return paramType.some(t => areTypesCompatible(t, inferredType));
    }
    return areTypesCompatible(paramType, inferredType);
}

/**
 * Returns the display string for a parameter type.
 * Union types are rendered as "A | B | C".
 */
function formatParamType(paramType) {
    if (Array.isArray(paramType)) return paramType.join(" | ");
    return paramType;
}

/**
 * Returns the first token-like object we can use for position from an expr node.
 * CALL nodes carry startToken (opening-paren token) and callee (IDENTIFIER node).
 */
function getArgToken(node) {
    if (!node) return null;
    if (node.token) return node.token;            // LITERAL, IDENTIFIER, UNARY, BINARY, PAREN
    if (node.callee?.token) return node.callee.token; // CALL — callee name token
    if (node.startToken) return node.startToken;  // CALL fallback
    return null;
}

// ---------------------------------------------------------------------------
// AST walking
// ---------------------------------------------------------------------------

/**
 * Recursively collects TokenSpan values from expression-bearing statement nodes.
 * Includes AssignStatement, SetStatement, and ExpressionStatement (standalone calls).
 * Returns objects { span, lineIndex } so the caller can skip output lines.
 */
function collectExpressionSpans(statements, out = []) {
    for (const node of statements) {
        if (
            node.type === NodeType.AssignStatement ||
            node.type === NodeType.SetStatement    ||
            node.type === NodeType.ExpressionStatement
        ) {
            if (node.value) out.push({ span: node.value, lineIndex: node.line });
        }

        if (node.consequent?.length) collectExpressionSpans(node.consequent, out);
        if (node.alternate?.length)  collectExpressionSpans(node.alternate, out);
        if (Array.isArray(node.body)) collectExpressionSpans(node.body, out);
        if (node.branches) {
            for (const branch of node.branches) {
                if (branch.body) collectExpressionSpans(branch.body, out);
            }
        }
        if (Array.isArray(node.otherwise)) collectExpressionSpans(node.otherwise, out);
    }
    return out;
}

/**
 * Recursively visits all CALL nodes in an expression tree.
 * @param {object} node  ExprNode
 * @param {Function} visitor  Called with each CALL node
 */
function walkCalls(node, visitor) {
    if (!node) return;
    switch (node.type) {
        case ExprNodeType.CALL:
            visitor(node);
            for (const arg of node.args || []) walkCalls(arg, visitor);
            break;
        case ExprNodeType.BINARY:
            walkCalls(node.left, visitor);
            walkCalls(node.right, visitor);
            break;
        case ExprNodeType.UNARY:
            walkCalls(node.operand, visitor);
            break;
        case ExprNodeType.PAREN:
            walkCalls(node.expr, visitor);
            break;
        default:
            break;
    }
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Validates function argument types against the built-in function registry.
 * Emits ACU-FNC-009 for each argument whose inferred type is incompatible
 * with the declared parameter type.
 *
 * @param {object} document  VS Code TextDocument or test shim
 * @param {Function} getRuleSeverity
 * @param {object} [options]
 * @param {Map<string, {type: string}>} [options.variableTypes]  Scoped variable type map
 * @param {object} [options.userDefinedFunctionCatalog]
 * @param {object[]|null} [options.executionClassificationMap]
 * @param {object[]} [options.functionScopes]
 * @returns {object[]}
 */
function validateArgumentTypes(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const variableTypes = options.variableTypes || new Map();
    const userDefinedFunctionCatalog = options.userDefinedFunctionCatalog || {};
    const executionClassificationMap = options.executionClassificationMap || null;
    const functionScopes = options.functionScopes || [];

    const { ast } = buildDocumentAst(document);
    const spans = collectExpressionSpans(ast.statements);

    // Cache variable maps per scope to avoid rebuilding.
    const variableMapCache = new Map();

    for (const { span, lineIndex } of spans) {
        if (executionClassificationMap && executionClassificationMap[lineIndex]?.isOutput) {
            continue;
        }

        const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
        const scopeId = scope ? String(scope.startLine) : "global";

        if (!variableMapCache.has(scopeId)) {
            variableMapCache.set(scopeId, buildVariablesMap(variableTypes, scopeId));
        }
        const variables = variableMapCache.get(scopeId);
        const inferContext = { variables, userDefinedFunctionCatalog };

        const exprTree = parseExpression(span);
        if (!exprTree) continue;

        walkCalls(exprTree, (callNode) => {
            const name = callNode.callee?.name?.toUpperCase();
            if (!name) return;

            const sig = BuiltInFunctions[name];
            if (!sig || sig.varArgs) return;
            if (!sig.params || sig.params.length === 0) return;

            for (let i = 0; i < sig.params.length; i++) {
                const arg = callNode.args[i];
                if (!arg) break; // arg count mismatch — handled by ACU-FNC-006

                const inferred = inferExpressionType(arg, inferContext);
                if (inferred === null) continue; // unknown type — skip to avoid false positive

                if (!paramTypeMatches(inferred, sig.params[i])) {
                    const tok = getArgToken(arg);
                    if (!tok) continue;

                    const range = {
                        start: { line: tok.line, character: tok.char },
                        end:   { line: tok.line, character: tok.char + (tok.value?.length ?? 1) }
                    };

                    const diag = createDiagnostic(
                        range,
                        {
                            messageId: "ACU-FNC-009",
                            params: {
                                n: i + 1,
                                function: name,
                                expectedType: formatParamType(sig.params[i]),
                                actualType: inferred
                            }
                        },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
            }
        });
    }

    return diagnostics;
}

export { validateArgumentTypes };
