/**
 * Index lifecycle usage validator for Acurity Architect.
 *
 * Validates warning- and error-level patterns for INDEX function usage:
 *
 * - ACU-PAT-001: INDEX cursor operations without an INDEX_CLOSE in scope.
 * - ACU-PAT-002: INDEX_SAVE_POSITION and INDEX_RESTORE_POSITION used without a matching pair.
 * - ACU-PAT-003: Cursor movement (LOAD/READ) without any SAVE/RESTORE.
 * - ACU-PAT-004: INDEX_READ_BY_KEY before INDEX_LOAD_KEY_*.
 * - ACU-PAT-005: INDEX_READ_BY_KEY before INDEX_OPEN_*.
 * - ACU-PAT-006: INDEX_CLOSE before READ/LOAD operations.
 * - ACU-PAT-010: Use of a handle variable after INDEX_CLOSE (stale handle).
 * - ACU-PAT-013: INDEX_RESTORE_POSITION for a table code with no active save.
 * - ACU-PAT-014: INDEX_LOAD_KEY_* type mismatch against the index segment definition.
 * - ACU-PAT-015: INDEX_LOAD_KEY_* call count exceeds the segments in the index.
 */

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

// Index functions that take a handle at argument position 0.
// A stale handle (post-CLOSE) passed to any of these emits ACU-PAT-010.
const INDEX_HANDLE_ARG0_FUNCTIONS = new Set([
    "INDEX_READ_BY_KEY",
    "INDEX_LOAD_KEY_STRING",
    "INDEX_LOAD_KEY_LONG",
    "INDEX_LOAD_KEY_SHORT",
    "INDEX_LOAD_KEY_DATE",
    "INDEX_CLEAR_KEY",
    "SQL_INDEX_READ_BY_KEY",
    "SQL_INDEX_LOAD_KEY_STRING",
    "SQL_INDEX_LOAD_KEY_LONG",
    "SQL_INDEX_LOAD_KEY_SHORT",
    "SQL_INDEX_LOAD_KEY_DATE",
    "SQL_INDEX_CLEAR_KEY"
]);

// Predicates that match either the INDEX_* or SQL_INDEX_* form of each operation.
function isIndexOpen(n)    { return n.startsWith("INDEX_OPEN_") || n === "SQL_INDEX_OPEN_STANDARD"; }
function isIndexLoadKey(n) { return n.startsWith("INDEX_LOAD_KEY_") || n.startsWith("SQL_INDEX_LOAD_KEY_"); }
function isIndexReadByKey(n) { return n === "INDEX_READ_BY_KEY" || n === "SQL_INDEX_READ_BY_KEY"; }
function isIndexClose(n)   { return n === "INDEX_CLOSE" || n === "SQL_INDEX_CLOSE"; }
function isIndexClearKey(n){ return n === "INDEX_CLEAR_KEY" || n === "SQL_INDEX_CLEAR_KEY"; }

/** Returns the load-key type suffix (e.g. "STRING", "LONG") from any load-key call name. */
function getLoadKeySuffix(callName) {
    if (callName.startsWith("SQL_INDEX_LOAD_KEY_")) return callName.slice("SQL_INDEX_LOAD_KEY_".length);
    return callName.slice("INDEX_LOAD_KEY_".length);
}

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, {
            open: [],
            load: [],
            read: [],
            close: [],
            save: [],
            restore: []
        });
    }
    return scopeRecords.get(scopeKey);
}

function registerCall(record, type, lineIndex, lineText, callName, handleVar) {
    record[type].push({ lineIndex, lineText, callName, handleVar: handleVar || null });
}

// Maps INDEX_LOAD_KEY_* and SQL_INDEX_LOAD_KEY_* call names to the segment types they satisfy.
const LOAD_KEY_ACCEPTED_TYPES = {
    "INDEX_LOAD_KEY_STRING":     new Set(["STRING", "ZSTRING"]),
    "INDEX_LOAD_KEY_SHORT":      new Set(["SHORT"]),
    "INDEX_LOAD_KEY_LONG":       new Set(["LONG"]),
    "INDEX_LOAD_KEY_DATE":       new Set(["DATE"]),
    "SQL_INDEX_LOAD_KEY_STRING": new Set(["STRING", "ZSTRING"]),
    "SQL_INDEX_LOAD_KEY_SHORT":  new Set(["SHORT"]),
    "SQL_INDEX_LOAD_KEY_LONG":   new Set(["LONG"]),
    "SQL_INDEX_LOAD_KEY_DATE":   new Set(["DATE"])
};

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

    const scopeRecords = new Map();
    // Per-scope set of variable names whose index cursors have been closed.
    const closedHandlesByScopeKey = new Map();
    // Per-scope map of tableCode -> active save count for ACU-PAT-013.
    const saveCountsByScopeKey = new Map();
    // Per-scope map of handleVar -> { tableCode, indexName, nextLoadPosition } for ACU-PAT-014/015.
    const loadPositionsByScopeKey = new Map();
    // Per-scope set of currently active (opened, not-yet-closed) handle variable names for ACU-PAT-016.
    const activeHandlesByScopeKey = new Map();

    function getLoadPositions(scopeKey) {
        if (!loadPositionsByScopeKey.has(scopeKey)) loadPositionsByScopeKey.set(scopeKey, new Map());
        return loadPositionsByScopeKey.get(scopeKey);
    }

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

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

    function getSaveCounts(scopeKey) {
        if (!saveCountsByScopeKey.has(scopeKey)) saveCountsByScopeKey.set(scopeKey, new Map());
        return saveCountsByScopeKey.get(scopeKey);
    }

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

        // Detect LHS assignment target for ACU-PAT-016 (active handle overwrite).
        const lhsMatch = lineText.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)/);
        const lhsVar = lhsMatch ? lhsMatch[1] : null;

        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 (isIndexOpen(callName)) {
                const openArg = extractArgAt(lineText.slice(match.index), 2);
                registerCall(record, "open", lineIndex, rawLineText, callName, openArg ? openArg.trim() : null);
            } else if (isIndexLoadKey(callName)) {
                const loadArg = extractArgAt(lineText.slice(match.index), 0);
                registerCall(record, "load", lineIndex, rawLineText, callName, loadArg ? loadArg.trim() : null);
            } else if (isIndexReadByKey(callName)) {
                const readArg = extractArgAt(lineText.slice(match.index), 0);
                registerCall(record, "read", lineIndex, rawLineText, callName, readArg ? readArg.trim() : null);
            } else if (isIndexClose(callName)) {
                const closeArg = extractArgAt(lineText.slice(match.index), 0);
                registerCall(record, "close", lineIndex, rawLineText, callName, closeArg ? closeArg.trim() : null);
            } else if (callName === "INDEX_SAVE_POSITION") {
                registerCall(record, "save", lineIndex, rawLineText, callName);
            } else if (callName === "INDEX_RESTORE_POSITION") {
                registerCall(record, "restore", lineIndex, rawLineText, callName);
            }

            // --- Load-position tracking (ACU-PAT-014/015) — sequential ---
            if (tableStructures) {
                const loadPositions = getLoadPositions(scopeKey);
                if (isIndexOpen(callName)) {
                    const tableArg = extractArgAt(lineText.slice(match.index), 0);
                    const indexArg = extractArgAt(lineText.slice(match.index), 1);
                    const handleArg = extractArgAt(lineText.slice(match.index), 2);
                    if (tableArg && indexArg && handleArg) {
                        const tableCodeRaw = tableArg.trim();
                        const tableCodeMatch = tableCodeRaw.match(/^["']([^"']+)["']$/);
                        const tableCode = tableCodeMatch ? tableCodeMatch[1].toUpperCase() : null;
                        const indexName = String(indexArg.trim());
                        const handleVar = handleArg.trim();
                        if (tableCode && /^[A-Za-z_][A-Za-z0-9_]*$/.test(handleVar)) {
                            loadPositions.set(handleVar, { tableCode, indexName, nextLoadPosition: 0 });
                        }
                    }
                } else if (isIndexClearKey(callName)) {
                    const handleArg = extractArgAt(lineText.slice(match.index), 0);
                    if (handleArg) {
                        const handleVar = handleArg.trim();
                        const entry = loadPositions.get(handleVar);
                        if (entry) entry.nextLoadPosition = 0;
                    }
                } else if (isIndexLoadKey(callName) && LOAD_KEY_ACCEPTED_TYPES[callName]) {
                    const handleArg = extractArgAt(lineText.slice(match.index), 0);
                    if (handleArg) {
                        const handleVar = handleArg.trim();
                        const entry = loadPositions.get(handleVar);
                        if (entry) {
                            const structure = tableStructures[entry.tableCode];
                            const index = structure ? structure.indexes.find((i) => i.name === entry.indexName) : null;
                            if (index) {
                                const pos = entry.nextLoadPosition;
                                if (pos >= index.segments.length) {
                                    // ACU-PAT-015: exceeds segment count
                                    const diagnostic = createDiagnostic(
                                        createCallRange(rawLineText, lineIndex, callName),
                                        {
                                            messageId: "ACU-PAT-015",
                                            params: { indexName: entry.indexName, tableCode: entry.tableCode, total: index.segments.length }
                                        },
                                        getRuleSeverity
                                    );
                                    if (diagnostic) diagnostics.push(diagnostic);
                                } else {
                                    const segment = index.segments[pos];
                                    const acceptedTypes = LOAD_KEY_ACCEPTED_TYPES[callName];
                                    if (acceptedTypes && !acceptedTypes.has(segment.type)) {
                                        // ACU-PAT-014: type mismatch
                                        const actualSuffix = getLoadKeySuffix(callName);
                                        const diagnostic = createDiagnostic(
                                            createCallRange(rawLineText, lineIndex, callName),
                                            {
                                                messageId: "ACU-PAT-014",
                                                params: { pos, expected: segment.type, callName, actual: actualSuffix }
                                            },
                                            getRuleSeverity
                                        );
                                        if (diagnostic) diagnostics.push(diagnostic);
                                    } else if (
                                        (callName === "INDEX_LOAD_KEY_STRING" || callName === "SQL_INDEX_LOAD_KEY_STRING") &&
                                        segment.length != null
                                    ) {
                                        // ACU-PAT-017: length argument exceeds segment width
                                        const lengthArg = extractArgAt(lineText.slice(match.index), 2);
                                        if (lengthArg) {
                                            const lengthStr = lengthArg.trim();
                                            if (/^\d+$/.test(lengthStr)) {
                                                const lengthVal = parseInt(lengthStr, 10);
                                                if (lengthVal > segment.length) {
                                                    const diagnostic = createDiagnostic(
                                                        createCallRange(rawLineText, lineIndex, callName),
                                                        {
                                                            messageId: "ACU-PAT-017",
                                                            params: { length: lengthVal, pos, field: segment.field, maxWidth: segment.length }
                                                        },
                                                        getRuleSeverity
                                                    );
                                                    if (diagnostic) diagnostics.push(diagnostic);
                                                }
                                            }
                                        }
                                    }
                                }
                                entry.nextLoadPosition += 1;
                            }
                        }
                    }
                }
            }

            // --- Stale handle tracking (ACU-PAT-010) — sequential ---
            const closedHandles = getClosedHandles(scopeKey);

            if (isIndexClose(callName)) {
                const argText = extractArgAt(lineText.slice(match.index), 0);
                if (argText) {
                    const varName = argText.trim();
                    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) closedHandles.add(varName);
                }
            } else if (isIndexOpen(callName)) {
                const argText = extractArgAt(lineText.slice(match.index), 2);
                if (argText) {
                    const varName = argText.trim();
                    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) closedHandles.delete(varName);
                }
            } else if (INDEX_HANDLE_ARG0_FUNCTIONS.has(callName)) {
                const argText = extractArgAt(lineText.slice(match.index), 0);
                if (argText) {
                    const varName = argText.trim();
                    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName) && closedHandles.has(varName)) {
                        const diagnostic = createDiagnostic(
                            createCallRange(rawLineText, lineIndex, callName),
                            { messageId: "ACU-PAT-010", params: { handle: varName } },
                            getRuleSeverity
                        );
                        if (diagnostic) diagnostics.push(diagnostic);
                    }
                }
            }

            // --- Active handle tracking for ACU-PAT-016 — sequential ---
            if (isIndexOpen(callName)) {
                const openHandleArg = extractArgAt(lineText.slice(match.index), 2);
                if (openHandleArg) {
                    const handleVar = openHandleArg.trim();
                    if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(handleVar)) {
                        getActiveHandles(scopeKey).add(handleVar);
                    }
                }
            } else if (isIndexClose(callName)) {
                const closeHandleArg = extractArgAt(lineText.slice(match.index), 0);
                if (closeHandleArg) {
                    getActiveHandles(scopeKey).delete(closeHandleArg.trim());
                }
            }

            // --- Per-table-code save/restore tracking (ACU-PAT-013) — sequential ---
            if (callName === "INDEX_SAVE_POSITION" || callName === "INDEX_RESTORE_POSITION") {
                const argText = extractArgAt(lineText.slice(match.index), 0);
                if (argText) {
                    const raw = argText.trim();
                    // Only track string literals (quoted table codes).
                    const literalMatch = raw.match(/^["']([^"']+)["']$/);
                    if (literalMatch) {
                        const tableCode = literalMatch[1].toUpperCase();
                        const saveCounts = getSaveCounts(scopeKey);
                        if (callName === "INDEX_SAVE_POSITION") {
                            saveCounts.set(tableCode, (saveCounts.get(tableCode) || 0) + 1);
                        } else {
                            const count = saveCounts.get(tableCode) || 0;
                            if (count === 0) {
                                const diagnostic = createDiagnostic(
                                    createCallRange(rawLineText, lineIndex, callName),
                                    { messageId: "ACU-PAT-013", params: { tableCode } },
                                    getRuleSeverity
                                );
                                if (diagnostic) diagnostics.push(diagnostic);
                            } else {
                                saveCounts.set(tableCode, count - 1);
                            }
                        }
                    }
                }
            }
        }

        // ACU-PAT-016: LHS assignment to an active index handle overwrites the cursor reference.
        if (lhsVar && getActiveHandles(scopeKey).has(lhsVar)) {
            const diagnostic = createDiagnostic(
                createCallRange(rawLineText, lineIndex, lhsVar),
                { messageId: "ACU-PAT-016", params: { handle: lhsVar } },
                getRuleSeverity
            );
            if (diagnostic) diagnostics.push(diagnostic);
        }
    }

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

    for (const record of scopeRecords.values()) {
        const hasCursorOps = record.open.length > 0 || record.load.length > 0 || record.read.length > 0;

        // ACU-PAT-001: Missing INDEX_CLOSE
        if (hasCursorOps && record.close.length === 0) {
            emitDiagnostic("ACU-PAT-001", record.read[0] || record.load[0] || record.open[0]);
        }

        // ACU-PAT-002: Unmatched SAVE/RESTORE pairs (one side entirely absent)
        if ((record.save.length > 0 && record.restore.length === 0) ||
            (record.save.length === 0 && record.restore.length > 0)) {
            emitDiagnostic("ACU-PAT-002", record.save[0] || record.restore[0]);
        }

        // ACU-PAT-003: Consider using SAVE/RESTORE for cursor movement
        if ((record.load.length > 0 || record.read.length > 0) &&
            record.save.length === 0 && record.restore.length === 0) {
            emitDiagnostic("ACU-PAT-003", record.read[0] || record.load[0]);
        }

        // ACU-PAT-004: READ before LOAD
        if (record.read.length > 0 && record.load.length > 0) {
            const firstRead = record.read[0];
            const firstLoad = record.load[0];
            if (firstRead.lineIndex < firstLoad.lineIndex) emitDiagnostic("ACU-PAT-004", firstRead);
        }

        // ACU-PAT-005: READ before OPEN
        if (record.read.length > 0 && record.open.length > 0) {
            const firstRead = record.read[0];
            const firstOpen = record.open[0];
            if (firstRead.lineIndex < firstOpen.lineIndex) emitDiagnostic("ACU-PAT-005", firstRead);
        }

        // ACU-PAT-006: CLOSE before READ/LOAD (per-handle, session-aware)
        // For each close, scope the check to the current session for that handle:
        // from the most recent OPEN for the handle (sessionStart) to the next OPEN
        // for the same handle after the close (sessionEnd). This prevents reads/loads
        // from a later session (after a handle variable is reused) from triggering
        // a false positive against an earlier valid close.
        for (const closeOp of record.close) {
            const handle = closeOp.handleVar;
            let lastOp;
            if (handle) {
                const precedingOpens = record.open.filter((op) => op.handleVar === handle && op.lineIndex < closeOp.lineIndex);
                const sessionStart = precedingOpens.length > 0 ? precedingOpens[precedingOpens.length - 1].lineIndex : -1;
                const followingOpens = record.open.filter((op) => op.handleVar === handle && op.lineIndex > closeOp.lineIndex);
                const sessionEnd = followingOpens.length > 0 ? followingOpens[0].lineIndex : Infinity;
                const sessionReads = record.read.filter((op) => op.handleVar === handle && op.lineIndex > sessionStart && op.lineIndex < sessionEnd);
                const sessionLoads = record.load.filter((op) => op.handleVar === handle && op.lineIndex > sessionStart && op.lineIndex < sessionEnd);
                lastOp = sessionReads.length > 0
                    ? sessionReads[sessionReads.length - 1]
                    : sessionLoads[sessionLoads.length - 1];
            } else {
                lastOp = record.read[record.read.length - 1] || record.load[record.load.length - 1];
            }
            if (lastOp && closeOp.lineIndex < lastOp.lineIndex) emitDiagnostic("ACU-PAT-006", closeOp);
        }
    }

    return diagnostics;
}

export { validateIndexUsage };
