/**
 * Hash-mode validator for Acurity Architect.
 *
 * Validates hash-mode (HASH-ON / #HASH-OFF) usage:
 *   ACU-SYN-001  Redundant HASH-ON directive
 *   ACU-SYN-002  Invalid executable line
 *   ACU-SYN-003  NOTE used in HASH-ON context (should be #NOTE)
 *   ACU-SYN-006  Incomplete assignment (no RHS after =)
 *   ACU-SYN-007  Invalid continuation ('...' not at end of line)
 *
 * ACU-SYN-001 tracks hash mode with a running variable and matches start-of-line
 * regex so that "HASH-ON" inside comments or strings is not treated as a directive.
 * ACU-SYN-002 uses line-by-line iteration via the ECM isInvalidExecutable flag.
 * ACU-SYN-003 uses the token stream (HASH_PREFIX immediately before IDENTIFIER
 * "NOTE") which gives exact column positions.
 * ACU-SYN-006 and ACU-SYN-007 use the ECM and token stream respectively.
 */

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

/**
 * @param {object} document
 * @param {Function} getRuleSeverity
 * @param {object} [options={}]
 * @param {object} [options.facts] - Pre-computed document facts from collectDocumentFacts()
 * @returns {object[]}
 */
function validateHashMode(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const { facts = {} } = options;
    const ecm = facts.executionClassificationMap;
    if (!ecm) return diagnostics;
    // -----------------------------------------------------------------------
    // ACU-SYN-001: Redundant HASH-ON directive
    // ACU-SYN-002: Invalid executable line
    // Both require line-level data; iterate once to handle both.
    // -----------------------------------------------------------------------
    const { tokens } = buildDocumentAst(document);

    // Track hash mode manually so we know the mode BEFORE each line fires.
    // ECM hashMode stores the mode AFTER the directive applies, which is
    // not sufficient to detect redundant HASH-ON directives.
    let currentHashMode = "HASH-ON"; // default is HASH-ON

    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        const lineText = document.lineAt(lineIndex).text;
        const trimmed = lineText.trim();
        const classification = ecm[lineIndex];

        // ACU-SYN-001: HASH-ON directive when already in HASH-ON mode.
        // Match start-of-line only so that "HASH-ON" inside comments or strings
        // is not treated as a mode directive.
        if (/^\s*#?\s*HASH-ON\b/i.test(lineText)) {
            if (currentHashMode === "HASH-ON") {
                const hashOnMatch = trimmed.match(/^#?\s*(HASH-ON)/i);
                if (hashOnMatch) {
                    const startCol = lineText.indexOf(hashOnMatch[1]);
                    const diag = createDiagnostic(
                        { start: { line: lineIndex, character: startCol }, end: { line: lineIndex, character: startCol + hashOnMatch[1].length } },
                        { messageId: "ACU-SYN-001", params: {} },
                        getRuleSeverity
                    );
                    if (diag) diagnostics.push(diag);
                }
            }
            currentHashMode = "HASH-ON";
        } else if (/^\s*#?\s*HASH-OFF\b/i.test(lineText)) {
            currentHashMode = "HASH-OFF";
        }

        if (classification?.isInvalidExecutable) {
            const firstNonWhitespace = lineText.search(/\S/);
            const startCol = firstNonWhitespace >= 0 ? firstNonWhitespace : 0;
            const diag = createDiagnostic(
                { start: { line: lineIndex, character: startCol }, end: { line: lineIndex, character: lineText.length } },
                { messageId: "ACU-SYN-002", params: {} },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
        }
    }

    // -----------------------------------------------------------------------
    // ACU-SYN-003: NOTE in HASH-ON context
    // Detect HASH_PREFIX immediately before IDENTIFIER("NOTE") where the line
    // is in HASH-ON mode. Token stream gives exact column positions without regex.
    // -----------------------------------------------------------------------
    let prevMeaningful = null;
    for (const token of tokens) {
        if (token.type === TokenType.WHITESPACE || token.type === TokenType.NEWLINE) continue;

        if (
            token.type === TokenType.IDENTIFIER &&
            token.value.toUpperCase() === "NOTE" &&
            prevMeaningful &&
            prevMeaningful.type === TokenType.HASH_PREFIX
        ) {
            const cls = ecm[token.line];
            if (cls?.hashMode === "HASH-ON") {
                const diag = createDiagnostic(
                    { start: { line: token.line, character: token.char }, end: { line: token.line, character: token.char + token.value.length } },
                    { messageId: "ACU-SYN-003", params: {} },
                    getRuleSeverity
                );
                if (diag) diagnostics.push(diag);
            }
        }
        prevMeaningful = token;
    }

    // -----------------------------------------------------------------------
    // ACU-SYN-006: Incomplete assignment — no RHS after the assignment operator.
    // Pattern: optional SET, identifier, optional compound op, =, then nothing.
    // Uses effectiveContent (hash-stripped, comment-stripped) from the ECM.
    // -----------------------------------------------------------------------
    const INCOMPLETE_ASSIGNMENT = /^(?:SET\s+)?[A-Za-z_][A-Za-z0-9_]*\s*(?:\+|-|\*|\/)?=\s*$/i;
    for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
        const cls = ecm[lineIndex];
        if (!cls?.isExecutable || cls.isInvalidExecutable || cls.isOutput) continue;
        const effective = (cls.effectiveContent || "").trim();
        if (!effective || !INCOMPLETE_ASSIGNMENT.test(effective)) continue;
        const rawLineText = document.lineAt(lineIndex).text;
        const startCol = rawLineText.search(/\S/);
        const diag = createDiagnostic(
            { start: { line: lineIndex, character: startCol >= 0 ? startCol : 0 }, end: { line: lineIndex, character: rawLineText.length } },
            { messageId: "ACU-SYN-006", params: {} },
            getRuleSeverity
        );
        if (diag) diagnostics.push(diag);
    }

    // -----------------------------------------------------------------------
    // ACU-SYN-007: Invalid continuation — '...' must be the last non-comment
    // token on a code line. Detect via token stream: for each CONTINUATION
    // token on a non-output line, check if any non-whitespace/non-comment
    // token follows it before the end of the same line.
    // -----------------------------------------------------------------------
    for (let i = 0; i < tokens.length; i++) {
        const tok = tokens[i];
        if (tok.type !== TokenType.CONTINUATION) continue;

        // Skip if this line is classified as output
        const cls = ecm[tok.line];
        if (cls?.isOutput && !cls?.isExecutable) continue;

        // Scan forward on the same line for any meaningful token
        let hasFollowingCode = false;
        for (let j = i + 1; j < tokens.length; j++) {
            const next = tokens[j];
            if (next.line !== tok.line) break;
            if (next.type === TokenType.NEWLINE || next.type === TokenType.EOF) break;
            if (next.type === TokenType.WHITESPACE || next.type === TokenType.COMMENT) continue;
            hasFollowingCode = true;
            break;
        }

        if (!hasFollowingCode) continue;

        const diag = createDiagnostic(
            { start: { line: tok.line, character: tok.char }, end: { line: tok.line, character: tok.char + 3 } },
            { messageId: "ACU-SYN-007", params: {} },
            getRuleSeverity
        );
        if (diag) diagnostics.push(diag);
    }

    return diagnostics;
}

export { validateHashMode };
