/**
 * Block indentation validator for Acurity Architect.
 *
 * Warns (ACU-BLK-004) when a closing keyword (ENDIF, ENDDO, ENDCASE,
 * ENDFUNCTION) or a mid-block keyword (ELSE) appears at a different
 * leading-whitespace column than the opening keyword it belongs to.
 *
 * Structural correctness (mismatched/stray END keywords) is the responsibility
 * of structureValidator. This validator only cares about visual indentation.
 *
 * Uses the token stream so hash-mode output lines are naturally excluded.
 * The indent level is derived from the HASH_PREFIX token's column (HASH-ON mode)
 * or the KEYWORD token's column (HASH-OFF mode).
 */

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

// Keywords that open a new block scope
const START_KEYWORDS = new Set(["IF", "DO", "CASE", "FUNCTION"]);

// Maps each closing keyword to its expected opening keyword
const END_TO_START = {
    "ENDIF":       "IF",
    "ENDDO":       "DO",
    "ENDCASE":     "CASE",
    "ENDFUNCTION": "FUNCTION"
};

// Mid-block keywords (ELSE) are valid inside these opening keywords
const MID_BLOCK_PARENTS = new Set(["IF", "CASE"]);

/**
 * Validates block indentation in a document.
 *
 * @param {object} document            VS Code TextDocument or PlainTextDocument
 * @param {function} getRuleSeverity   (ruleId: string) => number|null
 * @param {object} [options]
 * @param {object} [options.executionClassificationMap]
 * @returns {object[]}  Array of diagnostic plain objects
 */
function validateBlockIndentation(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const blockStack = [];
    const { tokens } = buildDocumentAst(document);
    const ecm = options.executionClassificationMap || null;

    // Pre-compute the column of the first HASH_PREFIX token per line.
    // This is the effective indent for HASH-ON code lines (where the '#' immediately
    // follows leading whitespace). For HASH-OFF lines the keyword token's own
    // char position gives the indent directly.
    const hashPrefixColByLine = new Map();
    for (const token of tokens) {
        if (token.type === TokenType.HASH_PREFIX && !hashPrefixColByLine.has(token.line)) {
            hashPrefixColByLine.set(token.line, token.char);
        }
    }

    // Process block keywords in token order; take only the first per line.
    const processedLines = new Set();

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

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

        const keyword = token.value.toUpperCase();
        const isBlock = START_KEYWORDS.has(keyword) || keyword in END_TO_START || keyword === "ELSE";
        if (!isBlock) continue;

        // One block keyword per line (matching the old line-at-a-time behaviour).
        if (processedLines.has(token.line)) continue;
        processedLines.add(token.line);

        // Indent = column of '#' (HASH-ON mode) or column of keyword (HASH-OFF mode).
        const indent = hashPrefixColByLine.has(token.line)
            ? hashPrefixColByLine.get(token.line)
            : token.char;

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

        if (START_KEYWORDS.has(keyword)) {
            blockStack.push({ keyword, indent, lineIndex: token.line });
        } else if (keyword === "ELSE") {
            if (blockStack.length > 0) {
                const top = blockStack[blockStack.length - 1];
                if (MID_BLOCK_PARENTS.has(top.keyword) && indent !== top.indent) {
                    const diag = createDiagnostic(
                        range,
                        {
                            messageId: "ACU-BLK-004",
                            params: {
                                keyword,
                                column: indent,
                                openKeyword: top.keyword,
                                lineNumber: top.lineIndex + 1,
                                openColumn: top.indent
                            }
                        },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
            }
        } else if (keyword in END_TO_START) {
            if (blockStack.length > 0) {
                const top = blockStack[blockStack.length - 1];
                if (indent !== top.indent) {
                    const diag = createDiagnostic(
                        range,
                        {
                            messageId: "ACU-BLK-004",
                            params: {
                                keyword,
                                column: indent,
                                openKeyword: top.keyword,
                                lineNumber: top.lineIndex + 1,
                                openColumn: top.indent
                            }
                        },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
                blockStack.pop();
            }
            // Empty stack means a stray END keyword — structureValidator handles that case.
        }
    }

    return diagnostics;
}

export { validateBlockIndentation };
