/**
 * Include graph parsing and path-resolution helpers.
 */

import * as fs from "fs";
import * as path from "path";
import { parseInclude as parseIncludeFilename, stripTrailingComment, parseFunctionDeclaration } from "../parser.js";
import { MAX_INCLUDE_RECURSION_DEPTH } from "./cache.js";

/**
 * Represents an include statement and its position in the file.
 */
class Include {
    constructor(filename, lineIndex) {
        this.filename = filename;
        this.lineIndex = lineIndex;
    }
}

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

function isIncludeOnce(lineText) {
    const effectiveText = stripTrailingComment(lineText);
    return /^\s*#?\s*includeonce\s+/i.test(effectiveText);
}

function parseFunctionDeclarationName(lineText) {
    const declaration = parseFunctionDeclaration(lineText);
    if (!declaration) {
        return null;
    }
    return {
        declaredName: declaration.name,
        upperName: declaration.upperName
    };
}

function parseFunctionName(lineText) {
    const declaration = parseFunctionDeclarationName(lineText);
    return declaration ? declaration.upperName : null;
}

// Cache for resolved path results. Only positive (non-null) results are stored
// so that files created or renamed during a session are picked up on the next
// resolution attempt without requiring a restart.
const resolvedPathCache = new Map();

function resolvePathCaseInsensitive(candidatePath) {
    const cacheKey = path.normalize(candidatePath).toLowerCase();
    if (resolvedPathCache.has(cacheKey)) {
        return resolvedPathCache.get(cacheKey);
    }

    const parsed = path.parse(candidatePath);
    let current = parsed.root;
    if (!current || !fs.existsSync(current)) {
        return null;
    }

    const remainder = candidatePath.slice(parsed.root.length);
    const segments = remainder.split(path.sep).filter(Boolean);

    for (const segment of segments) {
        let entries = [];
        try {
            entries = fs.readdirSync(current);
        } catch (error) {
            return null;
        }

        let matchedEntry = entries.find((entry) => entry === segment);
        if (!matchedEntry) {
            matchedEntry = entries.find((entry) => entry.toLowerCase() === segment.toLowerCase());
        }

        if (!matchedEntry) {
            return null;
        }

        current = path.join(current, matchedEntry);
    }

    const result = fs.existsSync(current) ? current : null;
    if (result !== null) {
        resolvedPathCache.set(cacheKey, result);
    }
    return result;
}

/**
 * Returns true if resolvedPath is within allowedRoot (inclusive).
 * Both paths should already be absolute.
 * @param {string} resolvedPath
 * @param {string} allowedRoot
 */
function isPathWithinRoot(resolvedPath, allowedRoot) {
    const normalizedResolved = path.normalize(resolvedPath);
    const normalizedRoot = path.normalize(allowedRoot);
    const rootWithSep = normalizedRoot.endsWith(path.sep) ? normalizedRoot : normalizedRoot + path.sep;
    return normalizedResolved === normalizedRoot || normalizedResolved.startsWith(rootWithSep);
}

/**
 * Resolves an include path, rejecting absolute filenames and optionally enforcing a workspace root.
 * @param {string} baseDir
 * @param {string} includeFilename
 * @param {string|null} [allowedRoot] - If provided, resolved path must be within this directory
 */
function resolveIncludePath(baseDir, includeFilename, allowedRoot = null) {
    // Acurity INCLUDE paths must be relative — reject absolute paths as a safety guard
    if (path.isAbsolute(includeFilename)) {
        return null;
    }
    const resolved = path.resolve(baseDir, includeFilename);
    if (allowedRoot && !isPathWithinRoot(resolved, allowedRoot)) {
        return null;
    }
    return resolvePathCaseInsensitive(resolved);
}

/**
 * Async variant of resolveIncludePath. Uses the same case-insensitive directory
 * walk as the sync version so both paths behave identically on case-sensitive
 * filesystems (Linux CI). The getFileMetadataAsync parameter is retained for
 * call-site compatibility but is no longer used for resolution.
 * @param {string} baseDir
 * @param {string} includeFilename
 * @param {Function} _getFileMetadataAsync - unused; retained for call-site compat
 * @param {string|null} [allowedRoot]
 */
async function resolveIncludePathAsync(baseDir, includeFilename, _getFileMetadataAsync, allowedRoot = null) {
    if (path.isAbsolute(includeFilename)) {
        return null;
    }
    const resolved = path.resolve(baseDir, includeFilename);
    if (allowedRoot && !isPathWithinRoot(resolved, allowedRoot)) {
        return null;
    }
    return resolvePathCaseInsensitive(resolved);
}

function parseIncludes(document) {
    const includes = [];

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        const lineText = document.lineAt(lineIndex).text;

        const filename = parseIncludeFilename(lineText)?.filename;
        if (filename) {
            includes.push(new Include(filename, lineIndex));
        }
    }

    return includes;
}

function getIncludesInScopeAtLine(allIncludes, lineIndex) {
    return allIncludes
        .filter((include) => include.lineIndex < lineIndex)
        .map((include) => include.filename);
}

function isFunctionInScope(functionName, lineIndex, userDefinedFunctions) {
    if (userDefinedFunctions[functionName] !== undefined) {
        const definitionLine = userDefinedFunctions[functionName];
        if (definitionLine < lineIndex) {
            return true;
        }
    }

    return false;
}

function findIncludeFilenameCaseMismatches(document) {
    const mismatches = [];
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return mismatches;
    }

    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 requestedName = path.basename(includeFilename);
        const actualName = path.basename(includePath);
        if (requestedName !== actualName && requestedName.toLowerCase() === actualName.toLowerCase()) {
            mismatches.push({
                lineIndex,
                includeFilename,
                actualFilename: actualName
            });
        }
    }

    return mismatches;
}

function findMissingIncludeDirectives(document) {
    const missingDirectives = [];
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return missingDirectives;
    }

    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) {
            missingDirectives.push({ lineIndex, includeFilename });
        }
    }

    return missingDirectives;
}

function hasCircularIncludeFromFile(filePath, activeStack, memo) {
    const normalizedPath = normalizePathKey(filePath);

    if (memo.has(normalizedPath)) {
        return memo.get(normalizedPath);
    }

    if (activeStack.has(normalizedPath)) {
        return true;
    }

    if (activeStack.size >= MAX_INCLUDE_RECURSION_DEPTH) {
        return true;
    }

    activeStack.add(normalizedPath);

    let fileContent = "";
    try {
        fileContent = fs.readFileSync(normalizedPath, "utf8");
    } catch (error) {
        activeStack.delete(normalizedPath);
        memo.set(normalizedPath, false);
        return false;
    }

    const lines = fileContent.split(/\r?\n/);
    const fileDir = path.dirname(normalizedPath);

    for (const lineText of lines) {
        const includeFilename = parseIncludeFilename(lineText)?.filename;
        if (!includeFilename) {
            continue;
        }

        if (isIncludeOnce(lineText)) {
            continue;
        }

        const childIncludePath = resolveIncludePath(fileDir, includeFilename);
        if (!childIncludePath) {
            continue;
        }

        if (hasCircularIncludeFromFile(childIncludePath, activeStack, memo)) {
            activeStack.delete(normalizedPath);
            memo.set(normalizedPath, true);
            return true;
        }
    }

    activeStack.delete(normalizedPath);
    memo.set(normalizedPath, false);
    return false;
}

function findCircularIncludeDirectives(document) {
    const circularDirectives = [];
    const documentPath = document.uri?.fsPath || document.fileName;
    if (!documentPath) {
        return circularDirectives;
    }

    const documentDir = path.dirname(documentPath);
    const memo = new Map();

    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;
        }

        if (hasCircularIncludeFromFile(includePath, new Set(), memo)) {
            circularDirectives.push({ lineIndex, includeFilename });
        }
    }

    return circularDirectives;
}

export {
    Include,
    normalizePathKey,
    parseIncludeFilename,
    isIncludeOnce,
    parseFunctionDeclarationName,
    parseFunctionName,
    resolvePathCaseInsensitive,
    isPathWithinRoot,
    resolveIncludePath,
    resolveIncludePathAsync,
    parseIncludes,
    getIncludesInScopeAtLine,
    isFunctionInScope,
    findIncludeFilenameCaseMismatches,
    findMissingIncludeDirectives,
    findCircularIncludeDirectives
};
