/**
 * Array lifecycle usage validator for Acurity Architect.
 *
 * Validates warning- and error-level patterns for SORTED_ARRAY function usage:
 *
 * - ACU-PAT-007: Array record operation on a handle that has no INIT_SORTED_ARRAY in the same scope.
 * - ACU-PAT-008: INIT_SORTED_ARRAY not followed by FREE_SORTED_ARRAY or FIN_SORTED_ARRAY.
 * - ACU-PAT-009: Array record operation on a handle after FREE_SORTED_ARRAY.
 * - ACU-PAT-011: GET_UNUSED_ARRAY_HANDLE result variable overwrites handle argument.
 * - ACU-PAT-012: Consecutive GET_UNUSED_ARRAY_HANDLE calls without INIT_SORTED_ARRAY between them.
 * - ACU-STY-005: Hard-coded integer literal used as an array handle argument.
 *
 * Design overview
 * ---------------
 * The validator makes two passes over the document:
 *
 * Pass 1 (line-by-line): Scans for array function calls and accumulates per-scope
 *   records (`scopeRecords`) tracking every INIT, FREE, FIN, and array operation seen.
 *   Inline diagnostics (ACU-PAT-009, ACU-PAT-011, ACU-PAT-012, ACU-STY-005) are emitted
 *   here because they depend on local context only.
 *
 * Pass 2 (post-processing): Walks the accumulated records to emit lifecycle diagnostics
 *   (ACU-PAT-007, ACU-PAT-008) that require a complete picture of the scope.
 *
 * State tracked per scope
 * -----------------------
 * - `scopeRecords`             Map<scopeKey, { arrayInit[], arrayFree[], arrayFin[], arrayOps[] }>
 *                              Collects all calls for post-processing in Pass 2.
 * - `freedArrayHandlesByScopeKey` Map<scopeKey, Set<handleName>>
 *                              Handles that have been FREE'd; used to detect use-after-free (ACU-PAT-009).
 * - `pendingInitHandlesByScopeKey` Map<scopeKey, Set<handleName>>
 *                              Handles obtained via GET_UNUSED_ARRAY_HANDLE but not yet INIT'd;
 *                              used to detect double-GET without INIT (ACU-PAT-012).
 */

import { stripTrailingComment, extractArgAt } from "../parser.js";
import { getFunctionScopeAtLine } from "../analysis/functionAnalysis.js";
import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";

function createCallRange(lineText, lineIndex, callName) {
    const start = Math.max(0, lineText.toUpperCase().indexOf(callName));
    return { start: { line: lineIndex, character: start }, end: { line: lineIndex, character: start + callName.length } };
}

function getScopeKey(scope) {
    return scope ? `function:${scope.id}` : "global";
}

function getScopeRecord(scopeRecords, scopeKey) {
    if (!scopeRecords.has(scopeKey)) {
        scopeRecords.set(scopeKey, {
            arrayInit: [],
            arrayFree: [],
            arrayFin: [],
            arrayOps: []
        });
    }
    return scopeRecords.get(scopeKey);
}

function registerCall(record, type, lineIndex, lineText, callName, handle = null) {
    record[type].push({ lineIndex, lineText, callName, handle });
}

function checkHardcodedArrayHandle(callName, lineText, matchIndex, rawLineText, lineIndex, argIndex, getRuleSeverity, diagnostics) {
    const argText = extractArgAt(lineText.slice(matchIndex), argIndex);
    if (argText && /^\d+$/.test(argText.trim())) {
        const diagnostic = createDiagnostic(
            createCallRange(rawLineText, lineIndex, callName),
            { messageId: "ACU-STY-005", params: { handle: argText.trim() } },
            getRuleSeverity
        );
        if (diagnostic) diagnostics.push(diagnostic);
    }
}

/**
 * Validates array lifecycle usage patterns.
 * @param {object} document
 * @param {Function} getRuleSeverity
 * @param {object} options
 * @returns {object[]} diagnostics
 */
function validateArrayUsage(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const functionScopes = options.functionScopes || [];
    const executionClassificationMap = options.executionClassificationMap || [];

    const scopeRecords = new Map();
    // Tracks freed handles per scope — prevents false ACU-PAT-009 after the handle is legitimately re-used.
    const freedArrayHandlesByScopeKey = new Map();
    // Tracks handles obtained via GET_UNUSED_ARRAY_HANDLE but not yet INIT'd — detects double-GET (ACU-PAT-012).
    const pendingInitHandlesByScopeKey = new Map();

    function getFreedArrayHandles(scopeKey) {
        if (!freedArrayHandlesByScopeKey.has(scopeKey)) freedArrayHandlesByScopeKey.set(scopeKey, new Set());
        return freedArrayHandlesByScopeKey.get(scopeKey);
    }

    function getPendingInitHandles(scopeKey) {
        if (!pendingInitHandlesByScopeKey.has(scopeKey)) pendingInitHandlesByScopeKey.set(scopeKey, new Set());
        return pendingInitHandlesByScopeKey.get(scopeKey);
    }

    // -------------------------------------------------------------------------
    // Pass 1: Scan each line for array function calls
    // Inline diagnostics (ACU-PAT-009, -011, -012, ACU-STY-005) are emitted here.
    // INIT/FREE/FIN/arrayOp entries are recorded for Pass 2.
    // -------------------------------------------------------------------------
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex += 1) {
        if (executionClassificationMap[lineIndex]?.isOutput) continue;

        const rawLineText = document.lineAt(lineIndex).text;
        const lineText = stripTrailingComment(rawLineText);

        const scope = getFunctionScopeAtLine(functionScopes, lineIndex);
        const scopeKey = getScopeKey(scope);
        const record = getScopeRecord(scopeRecords, scopeKey);

        const callPattern = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
        let match;

        while ((match = callPattern.exec(lineText)) !== null) {
            const callName = match[1].toUpperCase();

            if (callName === "GET_UNUSED_ARRAY_HANDLE") {
                // ACU-PAT-011: LHS variable same as handle argument overwrites the handle.
                // ACU-PAT-012: second GET_UNUSED before INIT for the previous handle.
                const argText = extractArgAt(lineText.slice(match.index), 0);
                if (argText) {
                    const argVar = argText.trim();
                    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(argVar)) {
                        const lhsPattern = new RegExp("\\b" + argVar + "\\s*=");
                        if (lhsPattern.test(rawLineText)) {
                            const diagnostic = createDiagnostic(
                                createCallRange(rawLineText, lineIndex, callName),
                                { messageId: "ACU-PAT-011", params: { variable: argVar } },
                                getRuleSeverity
                            );
                            if (diagnostic) diagnostics.push(diagnostic);
                        }
                        const pending = getPendingInitHandles(scopeKey);
                        if (pending.size > 0) {
                            const diagnostic = createDiagnostic(
                                createCallRange(rawLineText, lineIndex, callName),
                                { messageId: "ACU-PAT-012", params: {} },
                                getRuleSeverity
                            );
                            if (diagnostic) diagnostics.push(diagnostic);
                        }
                        pending.add(argVar);
                    }
                }
            } else if (callName === "INIT_SORTED_ARRAY") {
                const initArgText = extractArgAt(lineText.slice(match.index), 0);
                const initHandleName = initArgText ? initArgText.trim() : null;
                const normalizedInitHandle = initHandleName && /^[A-Za-z_][A-Za-z0-9_]*$/.test(initHandleName) ? initHandleName : null;
                registerCall(record, "arrayInit", lineIndex, rawLineText, callName, normalizedInitHandle);
                checkHardcodedArrayHandle(callName, lineText, match.index, rawLineText, lineIndex, 0, getRuleSeverity, diagnostics);
                if (initHandleName) getPendingInitHandles(scopeKey).delete(initHandleName);
            } else if (callName === "INITIALISE_ARRAY") {
                checkHardcodedArrayHandle(callName, lineText, match.index, rawLineText, lineIndex, 0, getRuleSeverity, diagnostics);
            } else if (callName === "FREE_SORTED_ARRAY") {
                registerCall(record, "arrayFree", lineIndex, rawLineText, callName);
                const argText = extractArgAt(lineText.slice(match.index), 0);
                if (argText) {
                    const handle = argText.trim();
                    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(handle)) {
                        getFreedArrayHandles(scopeKey).add(handle);
                        getPendingInitHandles(scopeKey).delete(handle);
                    }
                }
                checkHardcodedArrayHandle(callName, lineText, match.index, rawLineText, lineIndex, 0, getRuleSeverity, diagnostics);
            } else if (callName === "FIN_SORTED_ARRAY") {
                registerCall(record, "arrayFin", lineIndex, rawLineText, callName);
                getPendingInitHandles(scopeKey).clear();
            } else if (
                callName === "ADD_ARRAY_REC" ||
                callName === "READ_ARRAY_REC" ||
                callName.endsWith("_SAY")
            ) {
                const handleArgIndex = callName === "READ_ARRAY_REC" ? 1 : 0;
                const argText = extractArgAt(lineText.slice(match.index), handleArgIndex);
                const opHandle = argText && /^[A-Za-z_][A-Za-z0-9_]*$/.test(argText.trim()) ? argText.trim() : null;
                registerCall(record, "arrayOps", lineIndex, rawLineText, callName, opHandle);
                if (opHandle && getFreedArrayHandles(scopeKey).has(opHandle)) {
                    const diagnostic = createDiagnostic(
                        createCallRange(rawLineText, lineIndex, callName),
                        { messageId: "ACU-PAT-009", params: { handle: opHandle } },
                        getRuleSeverity
                    );
                    if (diagnostic) diagnostics.push(diagnostic);
                }
                if (callName === "ADD_ARRAY_REC" || callName === "READ_ARRAY_REC") {
                    checkHardcodedArrayHandle(callName, lineText, match.index, rawLineText, lineIndex, handleArgIndex, getRuleSeverity, diagnostics);
                }
            }
        }
    }

    function emitDiagnostic(messageId, source, params = {}) {
        const diagnostic = createDiagnostic(
            createCallRange(source.lineText, source.lineIndex, source.callName),
            { messageId, params },
            getRuleSeverity
        );
        if (diagnostic) diagnostics.push(diagnostic);
    }

    // -------------------------------------------------------------------------
    // Pass 2: Lifecycle completeness checks across the collected scope records
    // ACU-PAT-007 and ACU-PAT-008 require a complete view of each scope.
    // -------------------------------------------------------------------------
    for (const record of scopeRecords.values()) {
        // ACU-PAT-007: Per-handle — fire once per handle that has ops but no INIT in this scope
        const initializedHandles = new Set(record.arrayInit.map(i => i.handle).filter(Boolean));
        const reportedHandles = new Set();
        for (const op of record.arrayOps) {
            if (!op.handle) continue;
            if (initializedHandles.has(op.handle)) continue;
            if (reportedHandles.has(op.handle)) continue;
            emitDiagnostic("ACU-PAT-007", op, { handle: op.handle });
            reportedHandles.add(op.handle);
        }

        // ACU-PAT-008: INIT_SORTED_ARRAY not covered by a FREE or FIN
        for (const init of record.arrayInit) {
            const initLineStripped = stripTrailingComment(init.lineText);
            const initMatchIndex = initLineStripped.toUpperCase().indexOf(init.callName);
            const initHandle = initMatchIndex >= 0
                ? (extractArgAt(initLineStripped.slice(initMatchIndex), 0) || "").trim()
                : "";

            const hasSubsequentFree = record.arrayFree.some(free => {
                if (free.lineIndex <= init.lineIndex) return false;
                // If either handle could not be extracted, treat it as a wildcard to avoid false positives.
                if (!initHandle) return true;
                const freeLineStripped = stripTrailingComment(free.lineText);
                const freeMatchIndex = freeLineStripped.toUpperCase().indexOf(free.callName);
                const freeHandle = freeMatchIndex >= 0
                    ? (extractArgAt(freeLineStripped.slice(freeMatchIndex), 0) || "").trim()
                    : "";
                return !freeHandle || freeHandle === initHandle;
            });
            // FIN_SORTED_ARRAY takes no arguments and finalizes all arrays — any subsequent FIN covers any INIT.
            const hasSubsequentFin = record.arrayFin.some(fin => fin.lineIndex > init.lineIndex);
            if (!hasSubsequentFree && !hasSubsequentFin) {
                emitDiagnostic("ACU-PAT-008", init);
            }
        }

        // ACU-PAT-009 is handled sequentially above (op-after-free per handle).
    }

    return diagnostics;
}

export { validateArrayUsage };
