/**
 * Document facts collector for Acurity Architect.
 * Computes reusable document analysis data once per validation pass.
 * Results are memoized by (uri, version) so rapid successive calls for the
 * same unchanged document are essentially free.
 */

import * as path from "path";
import { buildDocumentAst } from "../ast/documentAst.js";
import { NodeType } from "../ast/nodes.js";
import { extractOutputDelimiters, buildOutputLineMap, buildExecutionClassificationMap, buildFunctionValidationTargetsByLine, buildInterpolationTargetsByLine } from "./executionContext.js";
import { buildIncludedSymbolsInScope } from "../includeResolver.js";
import { walkStatements } from "../ast/astHelpers.js";
import * as logger from "../logger.js";

/** @type {Map<string, {version: number, facts: object}>} */
const factsCache = new Map();

/**
 * Evicts stale cache entries to prevent unbounded growth.
 * Keeps at most MAX_CACHE_SIZE entries (LRU by insertion order via Map).
 */
const MAX_CACHE_SIZE = 50;
function evictIfNeeded() {
    if (factsCache.size > MAX_CACHE_SIZE) {
        const firstKey = factsCache.keys().next().value;
        factsCache.delete(firstKey);
    }
}

// ---------------------------------------------------------------------------
// AST-based analysis helpers (internal — not exported)
// ---------------------------------------------------------------------------

/**
 * Collects VarStatement nodes from a statement array into a locals map.
 * Records first-occurrence name→type mappings only.
 * @param {object[]} statements
 * @param {Map<string,string>} locals
 */
function collectLocals(statements, locals) {
    walkStatements(statements, stmt => {
        if (stmt.type === NodeType.VarStatement) {
            for (const name of stmt.names) {
                if (!locals.has(name)) locals.set(name, stmt.varType);
            }
        }
    });
}

/**
 * Builds function scopes from the AST.
 * Recurses into nested block bodies (IF, CASE, DO, etc.) so that FUNCTION
 * declarations inside guard patterns like `IF NOT SYMBOL(...)` are included.
 * Each FunctionDeclaration is paired with the next EndFunctionStatement at
 * the same nesting level.
 * @param {object} ast - Program AST
 * @param {number} lineCount - Total document lines (fallback for unclosed functions)
 * @returns {Array<{id,name,upperName,startLine,endLine,params:Map,locals:Map,returnType:string|null}>}
 */
function buildFunctionScopesFromAst(ast, lineCount) {
    const scopes = [];
    collectFunctionScopes(ast.statements, lineCount, scopes);
    return scopes;
}

function collectFunctionScopes(statements, lineCount, scopes) {
    for (let i = 0; i < statements.length; i++) {
        const stmt = statements[i];
        if (stmt.type === NodeType.FunctionDeclaration) {
            let endLine = lineCount - 1;
            for (let j = i + 1; j < statements.length; j++) {
                if (statements[j].type === NodeType.EndFunctionStatement) {
                    endLine = statements[j].line;
                    break;
                }
            }
            const params = new Map(stmt.params.map(p => [p.name, p.type]));
            const locals = new Map();
            collectLocals(stmt.body, locals);
            scopes.push({
                id: scopes.length,
                name: stmt.name,
                upperName: stmt.name.toUpperCase(),
                startLine: stmt.line,
                endLine,
                params,
                locals,
                returnType: stmt.returnType || null
            });
        } else {
            if (Array.isArray(stmt.body)) collectFunctionScopes(stmt.body, lineCount, scopes);
            if (Array.isArray(stmt.consequent)) collectFunctionScopes(stmt.consequent, lineCount, scopes);
            if (Array.isArray(stmt.alternate)) collectFunctionScopes(stmt.alternate, lineCount, scopes);
            if (stmt.branches) stmt.branches.forEach(b => Array.isArray(b.body) && collectFunctionScopes(b.body, lineCount, scopes));
            if (Array.isArray(stmt.otherwise)) collectFunctionScopes(stmt.otherwise, lineCount, scopes);
        }
    }
}

/**
 * Builds the first-declaration catalog for user-defined functions.
 * Recurses into nested block bodies to find FUNCTION declarations at any depth.
 * @param {object} ast - Program AST
 * @returns {Record<string, {lineIndex,declaredName,params,returnType}>}
 */
function buildFunctionCatalogFromAst(ast) {
    const catalog = {};
    collectFunctionCatalog(ast.statements, catalog);
    return catalog;
}

function collectFunctionCatalog(statements, catalog) {
    for (const stmt of statements) {
        if (stmt.type === NodeType.FunctionDeclaration) {
            const upperName = stmt.name.toUpperCase();
            if (catalog[upperName] === undefined) {
                catalog[upperName] = {
                    lineIndex: stmt.line,
                    declaredName: stmt.name,
                    params: stmt.params,
                    returnType: stmt.returnType
                };
            }
        } else {
            if (Array.isArray(stmt.body)) collectFunctionCatalog(stmt.body, catalog);
            if (Array.isArray(stmt.consequent)) collectFunctionCatalog(stmt.consequent, catalog);
            if (Array.isArray(stmt.alternate)) collectFunctionCatalog(stmt.alternate, catalog);
            if (stmt.branches) stmt.branches.forEach(b => Array.isArray(b.body) && collectFunctionCatalog(b.body, catalog));
            if (Array.isArray(stmt.otherwise)) collectFunctionCatalog(stmt.otherwise, catalog);
        }
    }
}

/**
 * Finds function declarations and flags duplicates (case-insensitive).
 * Recurses into nested block bodies to find FUNCTION declarations at any depth.
 * @param {object} ast - Program AST
 * @returns {Array<{functionName,lineIndex,isDuplicate}>}
 */
function findDuplicateFunctionDeclarationsFromAst(ast) {
    const declarations = [];
    const seen = {};
    collectDuplicateDeclarations(ast.statements, declarations, seen);
    return declarations;
}

function getSymbolGuardName(condition) {
    if (!condition || !condition.tokens) return null;
    const text = condition.tokens.map((t) => t.value).join("").replace(/\s+/g, " ").trim();
    const m = text.match(/^NOT\s*SYMBOL\s*\(\s*['"]([^'"]+)['"]\s*\)/i);
    return m ? m[1].toUpperCase() : null;
}

function collectDuplicateDeclarations(statements, declarations, seen, guardedName) {
    for (const stmt of statements) {
        if (stmt.type === NodeType.FunctionDeclaration) {
            const upperName = stmt.name.toUpperCase();
            const isDuplicate = seen[upperName] !== undefined;
            const isSymbolGuarded = guardedName !== undefined && guardedName === upperName;
            declarations.push({ functionName: upperName, lineIndex: stmt.line, isDuplicate: isDuplicate && !isSymbolGuarded });
            seen[upperName] = true;
        } else if (stmt.type === NodeType.IfStatement) {
            const symbolGuard = getSymbolGuardName(stmt.condition);
            if (Array.isArray(stmt.consequent)) collectDuplicateDeclarations(stmt.consequent, declarations, seen, symbolGuard);
            if (Array.isArray(stmt.alternate)) collectDuplicateDeclarations(stmt.alternate, declarations, seen, undefined);
        } else {
            if (Array.isArray(stmt.body)) collectDuplicateDeclarations(stmt.body, declarations, seen, undefined);
            if (Array.isArray(stmt.consequent)) collectDuplicateDeclarations(stmt.consequent, declarations, seen, undefined);
            if (Array.isArray(stmt.alternate)) collectDuplicateDeclarations(stmt.alternate, declarations, seen, undefined);
            if (stmt.branches) stmt.branches.forEach((b) => Array.isArray(b.body) && collectDuplicateDeclarations(b.body, declarations, seen, undefined));
            if (Array.isArray(stmt.otherwise)) collectDuplicateDeclarations(stmt.otherwise, declarations, seen, undefined);
        }
    }
}

/**
 * Builds the first-declaration catalog for macros.
 * @param {object} ast - Program AST
 * @returns {Record<string, {lineIndex,declaredName,params,body}>}
 */
function buildMacroCatalogFromAst(ast) {
    const catalog = {};

    for (const stmt of ast.statements) {
        if (stmt.type !== NodeType.MacroDeclaration) continue;
        const upperName = stmt.name.toUpperCase();
        if (catalog[upperName] !== undefined) continue;
        // Reconstruct body string from the TokenSpan (joining token values)
        const body = stmt.body ? stmt.body.tokens.map(t => t.value).join("") : "";
        catalog[upperName] = {
            lineIndex: stmt.line,
            declaredName: stmt.name,
            params: stmt.params,
            body
        };
    }

    return catalog;
}

/**
 * Finds duplicate macro declarations in a document.
 * @param {object} ast - Program AST
 * @returns {Array<{name,lineIndexes}>}
 */
function findDuplicateMacroDeclarationsFromAst(ast) {
    const declarations = new Map();

    for (const stmt of ast.statements) {
        if (stmt.type !== NodeType.MacroDeclaration) continue;
        const upperName = stmt.name.toUpperCase();
        if (!declarations.has(upperName)) declarations.set(upperName, []);
        declarations.get(upperName).push(stmt.line);
    }

    const duplicates = [];
    declarations.forEach((lineIndexes, name) => {
        if (lineIndexes.length > 1) duplicates.push({ name, lineIndexes });
    });
    return duplicates;
}

/**
 * Builds the declaredVariables and globalVariables maps from the AST.
 * Replaces the line-by-line parseVarDeclaration scan in collectDiagnostics.js.
 *
 * declaredVariables keys: "<scope.startLine>:<name>" for function-scoped vars
 *                         "global:<name>" for module-level vars
 * globalVariables keys: "<name>"  (module-level only)
 *
 * @param {object} ast - Program AST
 * @param {Array} functionScopes - Pre-built function scopes
 * @returns {{ declaredVariables: Map, globalVariables: Map }}
 */
function buildDeclaredVariables(ast, functionScopes) {
    const declaredVariables = new Map();
    const globalVariables = new Map();

    collectVarStatements(ast.statements, null, declaredVariables, globalVariables);

    for (const scope of functionScopes) {
        scope.params.forEach((paramType, paramName) => {
            const scopeKey = `${scope.startLine}:${paramName}`;
            if (!declaredVariables.has(scopeKey)) {
                declaredVariables.set(scopeKey, {
                    name: paramName,
                    type: paramType,
                    line: scope.startLine,
                    isFunctionScoped: true,
                    isFunctionParameter: true
                });
            }
        });
    }

    return { declaredVariables, globalVariables };
}

/**
 * Recursively walks statements collecting VarStatement nodes.
 * Tracks function scope context via currentScopeStartLine:
 *   null  → global scope
 *   <line number> → inside the function that starts at that line
 * Does NOT recurse into MacroDeclaration bodies.
 */
function collectVarStatements(statements, currentScopeStartLine, declaredVariables, globalVariables) {
    for (const stmt of statements) {
        if (stmt.type === NodeType.VarStatement) {
            for (const name of stmt.names) {
                const scopeId = currentScopeStartLine !== null ? currentScopeStartLine : "global";
                const scopeKey = `${scopeId}:${name}`;
                if (!declaredVariables.has(scopeKey)) {
                    declaredVariables.set(scopeKey, {
                        name,
                        type: stmt.varType,
                        line: stmt.line,
                        isFunctionScoped: currentScopeStartLine !== null,
                        isFunctionParameter: false
                    });
                }
                if (currentScopeStartLine === null && !globalVariables.has(name)) {
                    globalVariables.set(name, {
                        type: stmt.varType,
                        line: stmt.line,
                        isFunctionScoped: false,
                        isFunctionParameter: false
                    });
                }
            }
        } else if (stmt.type === NodeType.FunctionDeclaration) {
            collectVarStatements(stmt.body || [], stmt.line, declaredVariables, globalVariables);
        } else if (stmt.type === NodeType.MacroDeclaration) {
            // Macro bodies are a single TokenSpan — no nested AST nodes to descend into.
        } else {
            if (Array.isArray(stmt.body))       collectVarStatements(stmt.body,       currentScopeStartLine, declaredVariables, globalVariables);
            if (Array.isArray(stmt.consequent)) collectVarStatements(stmt.consequent, currentScopeStartLine, declaredVariables, globalVariables);
            if (Array.isArray(stmt.alternate))  collectVarStatements(stmt.alternate,  currentScopeStartLine, declaredVariables, globalVariables);
            if (stmt.branches) {
                for (const b of stmt.branches) {
                    if (Array.isArray(b.body)) collectVarStatements(b.body, currentScopeStartLine, declaredVariables, globalVariables);
                }
            }
            if (Array.isArray(stmt.otherwise)) collectVarStatements(stmt.otherwise, currentScopeStartLine, declaredVariables, globalVariables);
        }
    }
}

/**
 * Builds a map from line number → iterator variable name for all
 * DO iterator statements (DO varName = start TO end [BY step]).
 * Used by variableValidator to treat the iterator variable as implicitly
 * initialised on that line.
 * @param {object} ast - Program AST
 * @returns {Map<number, string>}
 */
function buildDoIteratorsByLine(ast) {
    const result = new Map();
    walkStatements(ast.statements, stmt => {
        if (stmt.type === NodeType.DoStatement && stmt.kind === "iterator" && stmt.iterator?.variable) {
            result.set(stmt.line, stmt.iterator.variable);
        }
    });
    return result;
}

/**
 * Builds two per-line maps from the AST in a single walk:
 *   nodeTypeByLine: Map<number, string>  — NodeType of the statement at that line
 *   stmtByLine: Map<number, object>      — The statement object itself
 * Used by functionValidator to replace regex-based line classification.
 * @param {object} ast - Program AST
 * @returns {{ nodeTypeByLine: Map, stmtByLine: Map }}
 */
function buildNodeMapsFromAst(ast) {
    const nodeTypeByLine = new Map();
    const stmtByLine = new Map();
    walkStatements(ast.statements, stmt => {
        if (typeof stmt.line === "number") {
            nodeTypeByLine.set(stmt.line, stmt.type);
            stmtByLine.set(stmt.line, stmt);
        }
    });
    return { nodeTypeByLine, stmtByLine };
}

/**
 * Builds a map from line number to assignment info for all SET and bare assignment statements.
 * Used by variableValidator to replace the parseAssignment() call.
 * @param {object} ast - Program AST
 * @returns {Map<number, {target: string, valueSpan: object|null}>}
 */
function buildAssignmentsByLine(ast) {
    const result = new Map();
    walkStatements(ast.statements, stmt => {
        if (stmt.type === NodeType.SetStatement || stmt.type === NodeType.AssignStatement) {
            result.set(stmt.line, { target: stmt.target, valueSpan: stmt.value });
        }
    });
    return result;
}

/**
 * Detects the `// @mode:strict`, `// @mode:legacy`, or `// @mode:default`
 * file-level annotation. Scans the first 10 non-blank lines.
 * The annotation is case-sensitive and must appear exactly as shown.
 * Returns "strict", "legacy", "default", or null when absent.
 * @param {object} document
 * @returns {"strict"|"legacy"|"default"|null}
 */
function detectMode(document) {
    let nonBlankCount = 0;
    for (let i = 0; i < document.lineCount; i++) {
        const text = document.lineAt(i).text.trim();
        if (text === "") continue;
        nonBlankCount++;
        if (text === "// @mode:strict")  return "strict";
        if (text === "// @mode:legacy")  return "legacy";
        if (text === "// @mode:default") return "default";
        if (nonBlankCount >= 10) break;
    }
    return null;
}

// ---------------------------------------------------------------------------
// Main public API
// ---------------------------------------------------------------------------

/**
 * Collects core facts about a document that can be reused across validators.
 * Results are memoized: if the document version has not changed since the last
 * call the cached result is returned immediately.
 * @param {object} document - The document to analyze
 * @returns {Object} - Immutable document facts
 */
function collectDocumentFacts(document) {
    const cacheKey = document.uri.toString();
    const cached = factsCache.get(cacheKey);
    if (cached && cached.version === document.version) {
        return cached.facts;
    }
    const factsStartMs = Date.now();

    const { ast } = buildDocumentAst(document);

    const functionScopes = buildFunctionScopesFromAst(ast, document.lineCount);
    const doIteratorsByLine = buildDoIteratorsByLine(ast);
    const { nodeTypeByLine, stmtByLine } = buildNodeMapsFromAst(ast);
    const assignmentsByLine = buildAssignmentsByLine(ast);
    const { declaredVariables, globalVariables } = buildDeclaredVariables(ast, functionScopes);
    const userDefinedFunctionCatalog = buildFunctionCatalogFromAst(ast);
    const userDefinedFunctionCaseMap = Object.fromEntries(
        Object.entries(userDefinedFunctionCatalog).map(([upper, meta]) => [upper, meta.declaredName])
    );
    const functionDuplicates = findDuplicateFunctionDeclarationsFromAst(ast);
    const macroCatalog = buildMacroCatalogFromAst(ast);
    const macroCaseMap = Object.fromEntries(
        Object.entries(macroCatalog).map(([upper, meta]) => [upper, meta.declaredName])
    );
    const macroDuplicates = findDuplicateMacroDeclarationsFromAst(ast);

    const executionClassificationMap = buildExecutionClassificationMap(document);
    const outputDelimiters = extractOutputDelimiters(document);
    const outputLineMap = buildOutputLineMap(document);
    const functionValidationTargetsByLine = buildFunctionValidationTargetsByLine(document, outputDelimiters, outputLineMap);
    const interpolationTargetsByLine = buildInterpolationTargetsByLine(document, outputLineMap, outputDelimiters);

    const includedSymbolsStartMs = Date.now();
    const {
        includedFunctionsByLine,
        includedVariablesByLine,
        includedFunctionCaseMapByLine,
        includedInitialisedVariablesByLine,
        includeDirectiveCount
    } = buildIncludedSymbolsInScope(document);
    const includedSymbolsMs = Date.now() - includedSymbolsStartMs;

    const mode = detectMode(document);

    const facts = Object.freeze({
        mode,
        functionScopes,
        doIteratorsByLine,
        nodeTypeByLine,
        stmtByLine,
        assignmentsByLine,
        executionClassificationMap,
        outputDelimiters,
        functionValidationTargetsByLine,
        outputLineMap,
        interpolationTargetsByLine,
        userDefinedFunctionCatalog,
        userDefinedFunctionCaseMap,
        functionDuplicates,
        macroCatalog,
        macroCaseMap,
        macroDuplicates,
        includedVariablesByLine,
        includedFunctionsByLine,
        includedFunctionCaseMapByLine,
        includedInitialisedVariablesByLine,
        declaredVariables,
        globalVariables
    });

    factsCache.set(cacheKey, { version: document.version, facts });
    evictIfNeeded();

    // Summary line for include-resolver evaluation. Grep for "include-resolver summary".
    // Key metrics:
    //   includeDirectiveCount  — number of #include lines in this document
    //   linesPerInclude        — lines / directives; high values mean Step 4 (range-based scope map) would reduce allocations most
    //   includedSymbolsMs      — time spent in buildIncludedSymbolsInScope specifically
    //   totalFactsMs           — full collectDocumentFacts wall-clock time for context
    logger.debug("include-resolver summary", {
        file: path.basename(document.uri?.fsPath || document.fileName || cacheKey),
        lineCount: document.lineCount,
        includeDirectiveCount,
        linesPerInclude: includeDirectiveCount > 0 ? Math.round(document.lineCount / includeDirectiveCount) : null,
        includedSymbolsMs,
        totalFactsMs: Date.now() - factsStartMs
    });

    return facts;
}

/**
 * Evicts the cached facts for a specific document URI.
 * Call this when a document's include dependencies change (e.g. an include file
 * is modified externally) so the next validation recomputes from scratch.
 * @param {string} uriString - document.uri.toString()
 */
function invalidateDocumentFacts(uriString) {
    factsCache.delete(uriString);
}

export {
    collectDocumentFacts,
    invalidateDocumentFacts,
    detectMode
};
