/**
 * Expression type inference for Acurity Architect.
 *
 * Expression type inference — Phase 2 of the expression tree plan.
 * Used by typeValidator (Phase 3) and argumentTypeValidator (Phase 4).
 *
 * Given an ExprNode (produced by parseExpression) and a variable context,
 * returns the inferred AcurityTypes value, or null when the type cannot be
 * determined (unknown variable, unknown function return type, type mismatch).
 *
 * null means "unknown" — validators treat it as a pass-through rather than
 * emitting a spurious error.
 */

import { ExprNodeType } from "../ast/expressionNodes.js";
import { AcurityTypes } from "../types/acurityTypes.js";
import { getGlobalVariableType } from "../builtins/functions.js";
import { getResolvedFunctionReturnType } from "./functionResolution.js";

// ---------------------------------------------------------------------------
// Numeric widening utilities
// ---------------------------------------------------------------------------

/** Ordered from narrowest to widest. INTEGER is a LONG synonym — normalized before lookup. */
const NUMERIC_WIDENING_ORDER = [
    AcurityTypes.SHORT,
    AcurityTypes.LONG,
    AcurityTypes.BIGINT,
    AcurityTypes.DOUBLE
];

/**
 * Returns true if `type` is one of the five numeric AcurityTypes.
 * @param {string|null} type
 * @returns {boolean}
 */
function isNumericType(type) {
    if (type === AcurityTypes.INTEGER) return true; // synonym for LONG
    return NUMERIC_WIDENING_ORDER.includes(type);
}

/**
 * Normalizes INTEGER → LONG for widening comparisons.
 * @param {string} type
 * @returns {string}
 */
function normalizeNumericType(type) {
    return type === AcurityTypes.INTEGER ? AcurityTypes.LONG : type;
}

/**
 * Returns the wider of two numeric types, or null if either is non-numeric.
 * @param {string|null} typeA
 * @param {string|null} typeB
 * @returns {string|null}
 */
function widenNumericTypes(typeA, typeB) {
    const a = normalizeNumericType(typeA);
    const b = normalizeNumericType(typeB);
    const ia = NUMERIC_WIDENING_ORDER.indexOf(a);
    const ib = NUMERIC_WIDENING_ORDER.indexOf(b);
    if (ia === -1 || ib === -1) return null;
    return NUMERIC_WIDENING_ORDER[Math.max(ia, ib)];
}

// ---------------------------------------------------------------------------
// Binary operator type rules
// ---------------------------------------------------------------------------

/**
 * @param {string} op - Operator string from the BINARY node
 * @param {object} leftNode - Left ExprNode
 * @param {object} rightNode - Right ExprNode
 * @param {object} context - Inference context
 * @returns {string|null}
 */
function inferBinaryType(op, leftNode, rightNode, context) {
    const upperOp = op.toUpperCase();

    // ── Comparisons ─────────────────────────────────────────────────────────
    // All comparison operators yield BOOLEAN regardless of operand types.
    if (op === "=" || op === "<>" || op === "<" || op === ">" ||
        op === "<=" || op === ">=") {
        return AcurityTypes.BOOLEAN;
    }

    // ── Logical ─────────────────────────────────────────────────────────────
    if (upperOp === "AND" || upperOp === "OR") {
        return AcurityTypes.BOOLEAN;
    }

    // ── Exponentiation ──────────────────────────────────────────────────────
    // ** always yields DOUBLE, regardless of operand types.
    if (op === "**") {
        return AcurityTypes.DOUBLE;
    }

    // For remaining operators we need operand types.
    const leftType  = inferExpressionType(leftNode,  context);
    const rightType = inferExpressionType(rightNode, context);

    // ── MOD ─────────────────────────────────────────────────────────────────
    // Result has the same type as the dividend (left operand).
    if (upperOp === "MOD") {
        if (isNumericType(leftType)) return normalizeNumericType(leftType);
        return null;
    }

    // ── Addition ────────────────────────────────────────────────────────────
    if (op === "+") {
        if (isNumericType(leftType) && isNumericType(rightType)) {
            return widenNumericTypes(leftType, rightType);
        }
        // String concatenation
        if (leftType === AcurityTypes.STRING && rightType === AcurityTypes.STRING) {
            return AcurityTypes.STRING;
        }
        // Date arithmetic: DATE + numeric → DATE
        if (leftType === AcurityTypes.DATE  && isNumericType(rightType)) return AcurityTypes.DATE;
        if (isNumericType(leftType)         && rightType === AcurityTypes.DATE) return AcurityTypes.DATE;
        // Time arithmetic: TIME + numeric → TIME
        if (leftType === AcurityTypes.TIME  && isNumericType(rightType)) return AcurityTypes.TIME;
        if (isNumericType(leftType)         && rightType === AcurityTypes.TIME) return AcurityTypes.TIME;
        return null;
    }

    // ── Subtraction ─────────────────────────────────────────────────────────
    if (op === "-") {
        if (isNumericType(leftType) && isNumericType(rightType)) {
            return widenNumericTypes(leftType, rightType);
        }
        // DATE - numeric → DATE
        if (leftType === AcurityTypes.DATE && isNumericType(rightType)) return AcurityTypes.DATE;
        // DATE - DATE → LONG (number of days between)
        if (leftType === AcurityTypes.DATE && rightType === AcurityTypes.DATE) return AcurityTypes.LONG;
        // TIME - numeric → TIME
        if (leftType === AcurityTypes.TIME && isNumericType(rightType)) return AcurityTypes.TIME;
        return null;
    }

    // ── Multiplication / Division ────────────────────────────────────────────
    if (op === "*" || op === "/") {
        if (isNumericType(leftType) && isNumericType(rightType)) {
            return widenNumericTypes(leftType, rightType);
        }
        return null;
    }

    return null;
}

// ---------------------------------------------------------------------------
// Main inference function
// ---------------------------------------------------------------------------

/**
 * Infers the result type of an expression node.
 *
 * @param {object|null} node - ExprNode produced by parseExpression
 * @param {object} [context]
 * @param {Map<string, string>} [context.variables]
 *   Uppercase variable name → AcurityTypes value.
 *   Build this from declaredVariables / functionScope params before calling.
 * @param {object} [context.userDefinedFunctionCatalog]
 *   User-defined function catalog (from documentFacts) for return-type lookup.
 * @returns {string|null} AcurityTypes value, or null when the type is unknown.
 */
function inferExpressionType(node, context = {}) {
    if (!node) return null;

    switch (node.type) {

        // ── Typed literals ───────────────────────────────────────────────────
        case ExprNodeType.LITERAL:
        case ExprNodeType.DATE:
        case ExprNodeType.TIME:
            return node.inferredType || null;

        // ── Parenthesised expression — passthrough ───────────────────────────
        case ExprNodeType.PAREN:
            return inferExpressionType(node.expr, context);

        // ── Identifier — variable or builtin constant ────────────────────────
        case ExprNodeType.IDENTIFIER: {
            const upperName = node.name.toUpperCase();
            // User-declared variable (or function parameter)
            const varType = context.variables instanceof Map
                ? context.variables.get(upperName)
                : undefined;
            if (varType) return varType;
            // Built-in system constant (MIN_SHORT, MAX_DATE, etc.)
            const constType = getGlobalVariableType(node.name);
            if (constType) return constType;
            return null;
        }

        // ── Function call — return type from registry ────────────────────────
        case ExprNodeType.CALL: {
            const calleeName = node.callee?.name;
            if (!calleeName) return null;
            return getResolvedFunctionReturnType(
                calleeName,
                context.userDefinedFunctionCatalog || {}
            );
        }

        // ── Unary operators ──────────────────────────────────────────────────
        case ExprNodeType.UNARY:
            if (node.op.toUpperCase() === "NOT") return AcurityTypes.BOOLEAN;
            // Unary minus preserves the operand's numeric type (null if non-numeric).
            if (node.op === "-") {
                const operandType = inferExpressionType(node.operand, context);
                return isNumericType(operandType) ? operandType : null;
            }
            return null;

        // ── Binary operators ─────────────────────────────────────────────────
        case ExprNodeType.BINARY:
            return inferBinaryType(node.op, node.left, node.right, context);

        default:
            return null;
    }
}

export { inferExpressionType, isNumericType, widenNumericTypes };
