/**
 * Block structure validator for Acurity Architect.
 * Validates matching IF/ENDIF, DO/ENDDO, CASE/ENDCASE, FUNCTION/ENDFUNCTION.
 * Uses the token stream so hash-mode output lines are naturally excluded.
 *
 * This file sits at core/src/ rather than core/src/validators/ because it
 * operates on the lexer token stream (structural grammar validation) rather than
 * line-level text analysis. It is a peer of the AST parser, not a linting rule.
 */

import { TokenType } from "./lexer/tokenTypes.js";
import { buildDocumentAst } from "./ast/documentAst.js";
import { createDiagnostic } from "./diagnostics/diagnosticFactory.js";

/**
 * Block keyword pairs for validation.
 */
const BLOCK_PAIRS = [
    { start: "IF", end: "ENDIF" },
    { start: "DO", end: "ENDDO" },
    { start: "CASE", end: "ENDCASE" },
    { start: "FUNCTION", end: "ENDFUNCTION" }
];

const BLOCK_OPEN_KEYWORDS = new Set(BLOCK_PAIRS.map(p => p.start));
const BLOCK_END_KEYWORDS  = new Set(BLOCK_PAIRS.map(p => p.end));

/**
 * Gets the expected END keyword for a start keyword.
 * @param {string} startKeyword
 * @returns {string}
 */
function getEndKeyword(startKeyword) {
    const pair = BLOCK_PAIRS.find((p) => p.start === startKeyword);
    return pair ? pair.end : `END${startKeyword}`;
}

/**
 * Validates block structure in a document.
 * @param {object} document - The document to validate
 * @param {Function} getRuleSeverity
 * @param {object} [options]
 * @param {object[]} [options.executionClassificationMap] - Per-line execution classification
 * @returns {object[]} - Array of diagnostics for block mismatches
 */
function validateBlockStructure(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const blockStack = [];
    const { tokens } = buildDocumentAst(document);
    const ecm = options.executionClassificationMap || null;

    let lastElseToken = null;

    for (const token of tokens) {
        if (token.type !== TokenType.KEYWORD) continue;

        // Skip tokens on output or non-executable lines (e.g. #NOTE, #?? debug lines).
        if (ecm) {
            const cls = ecm[token.line];
            if (cls && (cls.isOutput || !cls.isExecutable)) continue;
        }

        const keyword = token.value.toUpperCase();

        // Handle ELSE: validate placement, track for ELSE IF detection.
        if (keyword === "ELSE") {
            lastElseToken = token;
            const range = {
                start: { line: token.line, character: token.char },
                end:   { line: token.line, character: token.char + token.value.length }
            };
            const top = blockStack.length > 0 ? blockStack[blockStack.length - 1] : null;

            if (!top || top.keyword !== "IF") {
                // Stray ELSE — no matching IF block open.
                const diag = createDiagnostic(
                    range,
                    { messageId: "ACU-BLK-001", params: { closeKeyword: "ELSE", openKeyword: "IF" } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            } else if (top.hasElse) {
                // Duplicate ELSE within the same IF block.
                const diag = createDiagnostic(
                    range,
                    { messageId: "ACU-BLK-006", params: {} },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            } else {
                top.hasElse = true;
            }
            continue; // ELSE is neither a block opener nor closer
        }

        // Detect ELSE IF misuse: IF appearing on the same line immediately after ELSE.
        if (keyword === "IF" && lastElseToken !== null && lastElseToken.line === token.line) {
            const range = {
                start: { line: lastElseToken.line, character: lastElseToken.char },
                end:   { line: token.line, character: token.char + token.value.length }
            };
            const diag = createDiagnostic(
                range,
                { messageId: "ACU-BLK-005", params: {} },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
            lastElseToken = null;
            // Do NOT push the inner IF to the block stack — the outer IF's ENDIF closes it.
            continue;
        }

        // Clear lastElseToken when we encounter any keyword on a different line.
        if (lastElseToken !== null && token.line > lastElseToken.line) {
            lastElseToken = null;
        }

        // UNTIL is a bottom-of-loop condition that closes a DO block (same as ENDDO in the AST).
        if (keyword === "UNTIL") {
            const top = blockStack.length > 0 ? blockStack[blockStack.length - 1] : null;
            if (top && top.keyword === "DO") {
                blockStack.pop();
            }
            continue;
        }

        const isOpen = BLOCK_OPEN_KEYWORDS.has(keyword);
        const isEnd  = BLOCK_END_KEYWORDS.has(keyword);
        if (!isOpen && !isEnd) continue;

        const range = {
            start: { line: token.line, character: token.char },
            end:   { line: token.line, character: token.char + token.value.length }
        };

        if (isOpen) {
            blockStack.push({ keyword, lineIndex: token.line, range, hasElse: false });
        } else {
            const expectedStart = keyword.substring(3); // Remove "END" prefix

            if (blockStack.length === 0) {
                const diag = createDiagnostic(
                    range,
                    { messageId: "ACU-BLK-001", params: { closeKeyword: keyword, openKeyword: expectedStart } },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
                continue;
            }

            const lastBlock = blockStack[blockStack.length - 1];
            if (lastBlock.keyword !== expectedStart) {
                const diag = createDiagnostic(
                    range,
                    {
                        messageId: "ACU-BLK-002",
                        params: {
                            closeKeyword: keyword,
                            openKeyword: lastBlock.keyword,
                            lineNumber: lastBlock.lineIndex + 1
                        }
                    },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            }

            blockStack.pop();
        }
    }

    // Report unclosed blocks at their opening keyword line.
    for (const block of blockStack) {
        const expectedEnd = getEndKeyword(block.keyword);
        const diag = createDiagnostic(
            block.range,
            { messageId: "ACU-BLK-003", params: { openKeyword: block.keyword, lineNumber: block.lineIndex + 1, closeKeyword: expectedEnd } },
            getRuleSeverity
        );
        if (diag) diagnostics.push(diag);
    }

    return diagnostics;
}

export {
    validateBlockStructure
};
