/**
 * Include graph traversal and symbol-scope builders.
 */

import * as fs from "fs";
import * as path from "path";
import { parseVarDeclaration, parseAssignment } from "../parser.js";
import { createTelemetryRun } from "../telemetry.js";
import {
    includeSymbolCache,
    includeSymbolWarmInFlight,
    MAX_INCLUDE_RECURSION_DEPTH,
    getFileMetadata,
    getFileMetadataAsync,
    evictStatCacheForPath,
    isCacheEntryFresh,
    cloneSymbols,
    clonePathSet
} from "./cache.js";
import {
    normalizePathKey,
    parseIncludeFilename,
    parseFunctionDeclarationName,
    parseFunctionName,
    resolveIncludePath,
    resolveIncludePathAsync
} from "./graph.js";

function emptyResolvedSymbols() {
    return {
        functions: new Set(),
        variables: new Map(),
        functionCaseMap: new Map(),
        initialisedVariables: new Set(),
        dependencies: new Set()
    };
}

function resolveSymbolsFromIncludeFile(filePath, activeStack) {
    const normalizedPath = normalizePathKey(filePath);
    const isRootCall = activeStack.size === 0;
    const telemetry = isRootCall
        ? createTelemetryRun("include.resolve.sync", { file: normalizedPath })
        : null;

    const metadata = getFileMetadata(normalizedPath);
    if (!metadata) {
        telemetry?.increment("metadata.miss");
        telemetry?.emit({ result: "file-missing" });
        return emptyResolvedSymbols();
    }

    const cached = includeSymbolCache.get(normalizedPath);
    if (isCacheEntryFresh(cached, metadata)) {
        telemetry?.increment("cache.hit");
        telemetry?.emit({
            result: "cache-hit",
            dependenciesCount: cached.dependencies ? cached.dependencies.size : 0
        });
        return {
            ...cloneSymbols(cached.symbols),
            dependencies: clonePathSet(cached.dependencies)
        };
    }

    telemetry?.increment("cache.miss");

    if (activeStack.has(normalizedPath)) {
        telemetry?.increment("guard.circular");
        telemetry?.emit({ result: "guard-circular" });
        return emptyResolvedSymbols();
    }

    if (activeStack.size >= MAX_INCLUDE_RECURSION_DEPTH) {
        telemetry?.increment("guard.maxDepth");
        telemetry?.emit({ result: "guard-max-depth" });
        return emptyResolvedSymbols();
    }

    activeStack.add(normalizedPath);

    let fileContent = "";
    try {
        fileContent = fs.readFileSync(normalizedPath, "utf8");
    } catch (error) {
        activeStack.delete(normalizedPath);
        telemetry?.increment("read.error");
        telemetry?.emit({ result: "read-error", message: error?.message });
        return emptyResolvedSymbols();
    }

    const functions = new Set();
    const variables = new Map();
    const functionCaseMap = new Map();
    const initialisedVariables = new Set();
    const dependencies = new Set();
    const lines = fileContent.split(/\r?\n/);
    const fileDir = path.dirname(normalizedPath);
    let functionDepth = 0;

    for (const lineText of lines) {
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (includeFilename) {
            const childIncludePath = resolveIncludePath(fileDir, includeFilename);
            if (childIncludePath) {
                const normalizedChildPath = normalizePathKey(childIncludePath);
                dependencies.add(normalizedChildPath);

                const nested = resolveSymbolsFromIncludeFile(normalizedChildPath, activeStack);
                for (const functionName of nested.functions) {
                    functions.add(functionName);
                }
                for (const [upperName, declaredName] of nested.functionCaseMap.entries()) {
                    if (!functionCaseMap.has(upperName)) {
                        functionCaseMap.set(upperName, declaredName);
                    }
                }
                for (const [name, info] of nested.variables.entries()) {
                    if (!variables.has(name)) {
                        variables.set(name, info);
                    }
                }
                for (const varName of nested.initialisedVariables) {
                    initialisedVariables.add(varName);
                }
                for (const nestedDependencyPath of nested.dependencies) {
                    dependencies.add(nestedDependencyPath);
                }
            }
        }

        const functionName = parseFunctionName(lineText);
        if (functionName) {
            functions.add(functionName);

            const declaration = parseFunctionDeclarationName(lineText);
            if (declaration && !functionCaseMap.has(declaration.upperName)) {
                functionCaseMap.set(declaration.upperName, declaration.declaredName);
            }
            functionDepth++;
        } else if (/^\s*(?:#\s*)?ENDFUNCTION\b/i.test(lineText) && functionDepth > 0) {
            functionDepth--;
        }

        const declaration = parseVarDeclaration(lineText);
        if (declaration) {
            declaration.variables.forEach((variableName) => {
                if (!variables.has(variableName)) {
                    variables.set(variableName, {
                        type: declaration.type,
                        isFunctionScoped: false,
                        isFunctionParameter: false
                    });
                }
            });
        }

        // Track file-scope assignments so callers know which declared variables are initialised.
        if (functionDepth === 0) {
            const assignment = parseAssignment(lineText);
            if (assignment && variables.has(assignment.target)) {
                initialisedVariables.add(assignment.target);
            }
        }
    }

    activeStack.delete(normalizedPath);

    const symbols = { functions, variables, functionCaseMap, initialisedVariables };
    includeSymbolCache.set(normalizedPath, {
        mtimeMs: metadata.mtimeMs,
        size: metadata.size,
        symbols: cloneSymbols(symbols),
        dependencies: clonePathSet(dependencies)
    });

    telemetry?.emit({
        result: "resolved",
        functionsCount: functions.size,
        variablesCount: variables.size,
        dependenciesCount: dependencies.size
    });

    return {
        ...symbols,
        dependencies
    };
}

async function resolveSymbolsFromIncludeFileAsync(filePath, activeStack) {
    const normalizedPath = normalizePathKey(filePath);
    const isRootCall = activeStack.size === 0;
    const telemetry = isRootCall
        ? createTelemetryRun("include.resolve.async", { file: normalizedPath })
        : null;

    const metadata = await getFileMetadataAsync(normalizedPath);
    if (!metadata) {
        telemetry?.increment("metadata.miss");
        telemetry?.emit({ result: "file-missing" });
        return emptyResolvedSymbols();
    }

    const cached = includeSymbolCache.get(normalizedPath);
    if (isCacheEntryFresh(cached, metadata)) {
        telemetry?.increment("cache.hit");
        telemetry?.emit({
            result: "cache-hit",
            dependenciesCount: cached.dependencies ? cached.dependencies.size : 0
        });
        return {
            ...cloneSymbols(cached.symbols),
            dependencies: clonePathSet(cached.dependencies)
        };
    }

    telemetry?.increment("cache.miss");

    if (activeStack.has(normalizedPath)) {
        telemetry?.increment("guard.circular");
        telemetry?.emit({ result: "guard-circular" });
        return emptyResolvedSymbols();
    }

    if (activeStack.size >= MAX_INCLUDE_RECURSION_DEPTH) {
        telemetry?.increment("guard.maxDepth");
        telemetry?.emit({ result: "guard-max-depth" });
        return emptyResolvedSymbols();
    }

    if (includeSymbolWarmInFlight.has(normalizedPath)) {
        telemetry?.increment("inFlight.reused");
        const inFlightResult = await includeSymbolWarmInFlight.get(normalizedPath);
        telemetry?.emit({ result: "inflight-reused" });
        return {
            ...cloneSymbols(inFlightResult.symbols),
            dependencies: clonePathSet(inFlightResult.dependencies)
        };
    }

    const pending = (async () => {
        activeStack.add(normalizedPath);

        let fileContent = "";
        try {
            fileContent = await fs.promises.readFile(normalizedPath, "utf8");
        } catch (error) {
            activeStack.delete(normalizedPath);
            return {
                symbols: { functions: new Set(), variables: new Map(), functionCaseMap: new Map() },
                dependencies: new Set(),
                metadata
            };
        }

        const functions = new Set();
        const variables = new Map();
        const functionCaseMap = new Map();
        const initialisedVariables = new Set();
        const dependencies = new Set();
        const lines = fileContent.split(/\r?\n/);
        const fileDir = path.dirname(normalizedPath);
        let functionDepth = 0;

        for (const lineText of lines) {
            const includeFilename = parseIncludeFilename(lineText)?.filename;
            if (includeFilename) {
                const childIncludePath = await resolveIncludePathAsync(fileDir, includeFilename, getFileMetadataAsync);
                if (childIncludePath) {
                    const normalizedChildPath = normalizePathKey(childIncludePath);
                    dependencies.add(normalizedChildPath);

                    const nested = await resolveSymbolsFromIncludeFileAsync(normalizedChildPath, activeStack);
                    for (const functionName of nested.functions) {
                        functions.add(functionName);
                    }
                    for (const [upperName, declaredName] of nested.functionCaseMap.entries()) {
                        if (!functionCaseMap.has(upperName)) {
                            functionCaseMap.set(upperName, declaredName);
                        }
                    }
                    for (const [name, info] of nested.variables.entries()) {
                        if (!variables.has(name)) {
                            variables.set(name, info);
                        }
                    }
                    for (const varName of nested.initialisedVariables) {
                        initialisedVariables.add(varName);
                    }
                    for (const nestedDependencyPath of nested.dependencies) {
                        dependencies.add(nestedDependencyPath);
                    }
                }
            }

            const functionName = parseFunctionName(lineText);
            if (functionName) {
                functions.add(functionName);

                const declaration = parseFunctionDeclarationName(lineText);
                if (declaration && !functionCaseMap.has(declaration.upperName)) {
                    functionCaseMap.set(declaration.upperName, declaration.declaredName);
                }
                functionDepth++;
            } else if (/^\s*(?:#\s*)?ENDFUNCTION\b/i.test(lineText) && functionDepth > 0) {
                functionDepth--;
            }

            const declaration = parseVarDeclaration(lineText);
            if (declaration) {
                declaration.variables.forEach((variableName) => {
                    if (!variables.has(variableName)) {
                        variables.set(variableName, {
                            type: declaration.type,
                            isFunctionScoped: false,
                            isFunctionParameter: false
                        });
                    }
                });
            }

            if (functionDepth === 0) {
                const assignment = parseAssignment(lineText);
                if (assignment && variables.has(assignment.target)) {
                    initialisedVariables.add(assignment.target);
                }
            }
        }

        activeStack.delete(normalizedPath);
        return {
            symbols: { functions, variables, functionCaseMap, initialisedVariables },
            dependencies,
            metadata
        };
    })();

    includeSymbolWarmInFlight.set(normalizedPath, pending);

    let resolved;
    try {
        resolved = await pending;
    } finally {
        includeSymbolWarmInFlight.delete(normalizedPath);
    }

    includeSymbolCache.set(normalizedPath, {
        mtimeMs: resolved.metadata.mtimeMs,
        size: resolved.metadata.size,
        symbols: cloneSymbols(resolved.symbols),
        dependencies: clonePathSet(resolved.dependencies)
    });

    telemetry?.emit({
        result: "resolved",
        functionsCount: resolved.symbols.functions.size,
        variablesCount: resolved.symbols.variables.size,
        dependenciesCount: resolved.dependencies.size
    });

    return {
        ...cloneSymbols(resolved.symbols),
        dependencies: clonePathSet(resolved.dependencies)
    };
}

function resolveFunctionsFromIncludeFile(filePath, activeStack) {
    return resolveSymbolsFromIncludeFile(filePath, activeStack).functions;
}

function resolveVariablesFromIncludeFile(filePath, activeStack) {
    return resolveSymbolsFromIncludeFile(filePath, activeStack).variables;
}

function resolveFunctionCasesFromIncludeFile(filePath, activeStack) {
    return resolveSymbolsFromIncludeFile(filePath, activeStack).functionCaseMap;
}

function collectIncludeDependenciesForDocument(document) {
    const dependencies = new Set();
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return dependencies;
    }

    const documentDir = path.dirname(documentPath);

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const lineText = document.lineAt(lineIndex).text;
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (!includeFilename) {
            continue;
        }

        const includePath = resolveIncludePath(documentDir, includeFilename);
        if (!includePath) {
            continue;
        }

        const normalizedIncludePath = normalizePathKey(includePath);
        dependencies.add(normalizedIncludePath);

        const resolvedSymbols = resolveSymbolsFromIncludeFile(normalizedIncludePath, new Set());
        for (const nestedDependencyPath of resolvedSymbols.dependencies) {
            dependencies.add(nestedDependencyPath);
        }
    }

    return dependencies;
}

function invalidateIncludeCacheForPath(changedPath) {
    const targetPath = normalizePathKey(changedPath);
    const pending = [targetPath];
    const invalidated = new Set();

    while (pending.length > 0) {
        const current = pending.pop();
        if (invalidated.has(current)) {
            continue;
        }

        invalidated.add(current);
        includeSymbolCache.delete(current);
        includeSymbolWarmInFlight.delete(current);
        evictStatCacheForPath(current);

        for (const [cachePath, cacheEntry] of includeSymbolCache.entries()) {
            if (cacheEntry.dependencies && cacheEntry.dependencies.has(current)) {
                pending.push(cachePath);
            }
        }
    }
}

async function warmIncludeCacheForDocument(document) {
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return;
    }

    const documentDir = path.dirname(documentPath);
    const includePaths = new Set();

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const lineText = document.lineAt(lineIndex).text;
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (!includeFilename) {
            continue;
        }

        const includePath = await resolveIncludePathAsync(documentDir, includeFilename, getFileMetadataAsync);
        if (!includePath) {
            continue;
        }

        includePaths.add(normalizePathKey(includePath));
    }

    for (const includePath of includePaths) {
        await resolveSymbolsFromIncludeFileAsync(includePath, new Set());
    }
}

/**
 * Single-pass variant that builds all three scope maps in one traversal.
 * Prefer this over calling the three individual builders separately —
 * each include line is resolved once rather than three times.
 * @returns {{ includedFunctionsByLine: Map, includedVariablesByLine: Map, includedFunctionCaseMapByLine: Map }}
 */
function buildIncludedSymbolsInScope(document) {
    const includedFunctionsByLine = new Map();
    const includedVariablesByLine = new Map();
    const includedFunctionCaseMapByLine = new Map();
    const includedInitialisedVariablesByLine = new Map();

    const activeFunctions = new Set();
    const activeVariables = new Map();
    const activeFunctionCases = new Map();
    const activeInitialisedVariables = new Set();

    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return { includedFunctionsByLine, includedVariablesByLine, includedFunctionCaseMapByLine, includedInitialisedVariablesByLine, includeDirectiveCount: 0 };
    }
    const documentDir = path.dirname(documentPath);

    let includeDirectiveCount = 0;

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        includedFunctionsByLine.set(lineIndex, new Set(activeFunctions));
        includedVariablesByLine.set(lineIndex, new Map(activeVariables));
        includedFunctionCaseMapByLine.set(lineIndex, new Map(activeFunctionCases));
        includedInitialisedVariablesByLine.set(lineIndex, new Set(activeInitialisedVariables));

        const lineText = document.lineAt(lineIndex).text;
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (!includeFilename) {
            continue;
        }

        includeDirectiveCount += 1;

        const includePath = resolveIncludePath(documentDir, includeFilename);
        if (!includePath) {
            continue;
        }

        const resolved = resolveSymbolsFromIncludeFile(includePath, new Set());

        for (const functionName of resolved.functions) {
            activeFunctions.add(functionName);
        }
        for (const [upperName, declaredName] of resolved.functionCaseMap.entries()) {
            if (!activeFunctionCases.has(upperName)) {
                activeFunctionCases.set(upperName, declaredName);
            }
        }
        for (const [name, info] of resolved.variables.entries()) {
            if (!activeVariables.has(name)) {
                activeVariables.set(name, info);
            }
        }
        for (const varName of resolved.initialisedVariables) {
            activeInitialisedVariables.add(varName);
        }
    }

    return { includedFunctionsByLine, includedVariablesByLine, includedFunctionCaseMapByLine, includedInitialisedVariablesByLine, includeDirectiveCount };
}

/**
 * @deprecated Use buildIncludedSymbolsInScope which resolves all symbol types in a single pass.
 */
function buildIncludedFunctionsInScope(document) {
    const scopeByLine = new Map();
    const activeFunctions = new Set();
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return scopeByLine;
    }
    const documentDir = path.dirname(documentPath);

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        scopeByLine.set(lineIndex, new Set(activeFunctions));

        const lineText = document.lineAt(lineIndex).text;
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (!includeFilename) {
            continue;
        }

        const includePath = resolveIncludePath(documentDir, includeFilename);
        if (!includePath) {
            continue;
        }

        const resolvedFunctions = resolveFunctionsFromIncludeFile(includePath, new Set());
        for (const functionName of resolvedFunctions) {
            activeFunctions.add(functionName);
        }
    }

    return scopeByLine;
}

/**
 * @deprecated Use buildIncludedSymbolsInScope which resolves all symbol types in a single pass.
 */
function buildIncludedFunctionCasesInScope(document) {
    const scopeByLine = new Map();
    const activeFunctionCases = new Map();
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return scopeByLine;
    }
    const documentDir = path.dirname(documentPath);

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        scopeByLine.set(lineIndex, new Map(activeFunctionCases));

        const lineText = document.lineAt(lineIndex).text;
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (!includeFilename) {
            continue;
        }

        const includePath = resolveIncludePath(documentDir, includeFilename);
        if (!includePath) {
            continue;
        }

        const resolvedFunctionCases = resolveFunctionCasesFromIncludeFile(includePath, new Set());
        for (const [upperName, declaredName] of resolvedFunctionCases.entries()) {
            if (!activeFunctionCases.has(upperName)) {
                activeFunctionCases.set(upperName, declaredName);
            }
        }
    }

    return scopeByLine;
}

export {
    resolveSymbolsFromIncludeFile,
    resolveSymbolsFromIncludeFileAsync,
    collectIncludeDependenciesForDocument,
    invalidateIncludeCacheForPath,
    warmIncludeCacheForDocument,
    buildIncludedSymbolsInScope,
    buildIncludedFunctionsInScope,
    buildIncludedFunctionCasesInScope
};
