/**
 * Definition provider for Acurity Architect.
 * Enables "Go to Definition" for variables, functions, and include files.
 */

import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import { LANGUAGE_ID } from "../../core/src/constants.js";
import { parseVarDeclaration, parseFunctionDeclaration, parseInclude, stripTrailingComment } from "../../core/src/parser.js";
import { createTelemetryRun } from "../../core/src/telemetry.js";
import { MAX_INCLUDE_RECURSION_DEPTH } from "../../core/src/includeResolver/cache.js";

function normalizePathKey(filePath) {
    return path.normalize(filePath).toLowerCase();
}

/**
 * Resolves the full path of an include file relative to a base directory.
 * @param {string} baseDir - The base directory
 * @param {string} includeFilename - The include filename
 * @returns {string|null} - The resolved path or null
 */
function resolveIncludePath(baseDir, includeFilename, allowedRoot = null) {
    if (path.isAbsolute(includeFilename)) {
        return null;
    }
    const resolved = path.resolve(baseDir, includeFilename);
    if (allowedRoot) {
        const normalizedResolved = path.normalize(resolved);
        const normalizedRoot = path.normalize(allowedRoot);
        if (!normalizedResolved.startsWith(normalizedRoot + path.sep) && normalizedResolved !== normalizedRoot) {
            return null;
        }
    }
    return fs.existsSync(resolved) ? resolved : null;
}

/**
 * Searches for a variable declaration in a document.
 * @param {vscode.TextDocument} document - The document to search
 * @param {string} variableName - The variable name
 * @returns {vscode.Location|null} - The location or null
 */
function findVariableDefinition(document, variableName) {
    // Search for VAR declarations
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        const lineText = document.lineAt(lineIndex).text;
        const declaration = parseVarDeclaration(lineText);
        
        if (declaration && declaration.variables.includes(variableName)) {
            const varIndex = lineText.indexOf(variableName);
            if (varIndex !== -1) {
                const position = new vscode.Position(lineIndex, varIndex);
                const range = new vscode.Range(position, position.translate(0, variableName.length));
                return new vscode.Location(document.uri, range);
            }
        }
    }

    // Search for function parameters
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        const lineText = document.lineAt(lineIndex).text;
        const funcDecl = parseFunctionDeclaration(lineText);
        
        if (funcDecl) {
            for (const param of funcDecl.params) {
                if (param.name === variableName) {
                    const paramIndex = lineText.indexOf(variableName);
                    if (paramIndex !== -1) {
                        const position = new vscode.Position(lineIndex, paramIndex);
                        const range = new vscode.Range(position, position.translate(0, variableName.length));
                        return new vscode.Location(document.uri, range);
                    }
                }
            }
        }
    }

    return null;
}

/**
 * Searches for a function definition in a document.
 * @param {vscode.TextDocument} document - The document to search
 * @param {string} functionName - The function name (case-insensitive)
 * @returns {vscode.Location|null} - The location or null
 */
function findFunctionDefinition(document, functionName) {
    const upperFunctionName = functionName.toUpperCase();
    
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        const lineText = document.lineAt(lineIndex).text;
        const funcDecl = parseFunctionDeclaration(lineText);
        
        if (funcDecl && funcDecl.name.toUpperCase() === upperFunctionName) {
            const funcIndex = lineText.toLowerCase().indexOf("function");
            if (funcIndex !== -1) {
                const nameStart = funcIndex + "function".length;
                const nameMatch = lineText.substring(nameStart).match(/\s*([a-zA-Z_][a-zA-Z0-9_]*)/);
                if (nameMatch) {
                    const actualNameStart = nameStart + nameMatch.index + nameMatch[0].indexOf(nameMatch[1]);
                    const position = new vscode.Position(lineIndex, actualNameStart);
                    const range = new vscode.Range(position, position.translate(0, funcDecl.name.length));
                    return new vscode.Location(document.uri, range);
                }
            }
        }
    }
    
    return null;
}

/**
 * Searches for a variable or function definition in included files.
 * @param {vscode.TextDocument} document - The current document
 * @param {string} identifier - The identifier name
 * @param {number} currentLine - The current line number
 * @param {boolean} isFunction - Whether to search for a function
 * @returns {vscode.Location|null} - The location or null
 */
async function findDefinitionInIncludes(document, identifier, currentLine, isFunction, visitedIncludePaths = new Set(), depth = 0, token = null, telemetry = null) {
    if (token?.isCancellationRequested) {
        return null;
    }

    if (depth >= MAX_INCLUDE_RECURSION_DEPTH) {
        return null;
    }

    // Iterative depth-first traversal avoids recursive call stack growth while
    // preserving include resolution behavior and cycle handling.
    const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath || null;
    const pending = [{ doc: document, maxLine: currentLine, depth }];

    while (pending.length > 0) {
        if (token?.isCancellationRequested) {
            return null;
        }

        const current = pending.pop();
        if (current.depth >= MAX_INCLUDE_RECURSION_DEPTH) {
            continue;
        }

        const includes = [];
        const baseDir = path.dirname(current.doc.uri.fsPath);

        for (let lineIndex = 0; lineIndex < current.maxLine && lineIndex < current.doc.lineCount; lineIndex += 1) {
            const lineText = current.doc.lineAt(lineIndex).text;
            const includeFilename = parseInclude(lineText)?.filename;

            if (!includeFilename) {
                continue;
            }

            const includePath = resolveIncludePath(baseDir, includeFilename, workspaceRoot);
            if (includePath) {
                includes.push(includePath);
            }
        }

        telemetry?.increment("includes.blocksScanned");
        telemetry?.increment("includes.candidates", includes.length);

        for (let idx = includes.length - 1; idx >= 0; idx -= 1) {
            if (token?.isCancellationRequested) {
                return null;
            }

            const includePath = includes[idx];
            const normalizedIncludePath = normalizePathKey(includePath);
            if (visitedIncludePaths.has(normalizedIncludePath)) {
                telemetry?.increment("includes.skippedVisited");
                continue;
            }

            try {
                visitedIncludePaths.add(normalizedIncludePath);
                telemetry?.increment("includes.visited");
                const includeUri = vscode.Uri.file(includePath);
                const includeDoc = vscode.workspace.textDocuments.find((doc) => doc.uri?.fsPath === includePath)
                    || await vscode.workspace.openTextDocument(includeUri);

                if (token?.isCancellationRequested) {
                    return null;
                }

                const location = isFunction
                    ? findFunctionDefinition(includeDoc, identifier)
                    : findVariableDefinition(includeDoc, identifier);

                if (location) {
                    return location;
                }

                pending.push({
                    doc: includeDoc,
                    maxLine: includeDoc.lineCount,
                    depth: current.depth + 1
                });
                telemetry?.increment("includes.pushed");
            } catch (error) {
                // Skip files that can't be opened
                telemetry?.increment("includes.openErrors");
                continue;
            }
        }
    }

    return null;
}

/**
 * Provides definition locations for symbols.
 * @param {vscode.TextDocument} document - The document
 * @param {vscode.Position} position - The cursor position
 * @returns {Promise<vscode.Location|vscode.Location[]|null>} - The definition location(s)
 */
async function provideDefinition(document, position, token) {
    const telemetry = createTelemetryRun("definition.lookup", {
        file: document.fileName || document.uri?.fsPath,
        line: position.line + 1,
        character: position.character
    });

    let resultKind = "none";

    if (token?.isCancellationRequested) {
        telemetry?.emit({ result: "cancelled" });
        return null;
    }

    try {
        telemetry?.startPhase("definition.total");
        const line = document.lineAt(position.line);
        const lineText = line.text;

        // Check if cursor is on an INCLUDE statement filename
        const includeFilename = parseInclude(lineText)?.filename;
        if (includeFilename) {
            // Check if position is within the filename range
            const includeIndex = lineText.toLowerCase().indexOf("include");
            if (includeIndex !== -1) {
                const filenameStart = lineText.indexOf(includeFilename, includeIndex);
                const filenameEnd = filenameStart + includeFilename.length;

                if (position.character >= filenameStart && position.character <= filenameEnd) {
                    const baseDir = path.dirname(document.uri.fsPath);
                    const defWorkspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath || null;
                    const includePath = resolveIncludePath(baseDir, includeFilename, defWorkspaceRoot);

                    if (includePath) {
                        const includeUri = vscode.Uri.file(includePath);
                        resultKind = "include-file";
                        // Return location at the start of the file
                        return new vscode.Location(includeUri, new vscode.Position(0, 0));
                    }
                }
            }
        }

        // Get the word at the cursor position
        const wordRange = document.getWordRangeAtPosition(position, /[A-Za-z_][A-Za-z0-9_]*/);
        if (!wordRange) {
            resultKind = "no-word";
            return null;
        }

        const word = document.getText(wordRange);

        // Check if it's a function call (followed by opening parenthesis)
        const charAfterWord = wordRange.end.character < lineText.length ? lineText[wordRange.end.character] : "";
        const isFunction = charAfterWord === "(";
        telemetry?.setTag("identifier", word);
        telemetry?.setTag("identifierKind", isFunction ? "function" : "variable");

        // Search in current document first
        telemetry?.startPhase("definition.local");
        if (isFunction) {
            const localFunctionDef = findFunctionDefinition(document, word);
            if (localFunctionDef) {
                telemetry?.endPhase("definition.local");
                resultKind = "local";
                return localFunctionDef;
            }
        } else {
            const localVarDef = findVariableDefinition(document, word);
            if (localVarDef) {
                telemetry?.endPhase("definition.local");
                resultKind = "local";
                return localVarDef;
            }
        }
        telemetry?.endPhase("definition.local");

        // Search in included files
        telemetry?.startPhase("definition.includes");
        const includeLocation = await findDefinitionInIncludes(document, word, position.line, isFunction, new Set(), 0, token, telemetry);
        telemetry?.endPhase("definition.includes");
        if (includeLocation) {
            resultKind = "include";
            return includeLocation;
        }

        resultKind = "not-found";
        return null;
    } finally {
        telemetry?.endPhase("definition.total");
        telemetry?.emit({ result: resultKind });
    }
}

/**
 * Initializes the definition provider.
 * @param {vscode.ExtensionContext} context - The extension context
 */
function initializeDefinitionProvider(context) {
    const definitionProvider = vscode.languages.registerDefinitionProvider(
        LANGUAGE_ID,
        {
            provideDefinition
        }
    );

    context.subscriptions.push(definitionProvider);
}

export {
    initializeDefinitionProvider,
    provideDefinition,
    findVariableDefinition,
    findFunctionDefinition
};
