/**
 * DO iterator validator for Acurity Architect.
 * Checks that DO iterator loops (DO var = start TO end [BY step]) are well-formed.
 *
 * ACU-SYN-004 — doIteratorMissingClause
 *   Fires when a DO iterator has neither a TO clause nor a BY clause.
 *   At minimum one of these is required to bound the loop.
 *
 * ACU-SYN-005 — doIteratorNoUpperBound
 *   Fires when a DO iterator has a BY clause but no TO clause and no BREAK
 *   in the loop body. Such a loop runs indefinitely without an explicit exit.
 *   Does not fire when the loop is closed by UNTIL (exit condition present).
 */

import { NodeType } from "../ast/nodes.js";
import { buildDocumentAst } from "../ast/documentAst.js";
import { createDiagnostic } from "../diagnostics/diagnosticFactory.js";
import { walkStatements } from "../ast/astHelpers.js";

/**
 * Walk a statement list, yielding all DoStatement nodes with kind "iterator".
 * @param {object[]} stmts
 * @param {object[]} out - accumulated results
 */
function collectIteratorNodes(stmts, out) {
    walkStatements(stmts, (stmt) => {
        if (stmt.type === NodeType.DoStatement && stmt.kind === "iterator") {
            out.push(stmt);
        }
    });
}

/**
 * Return true if stmts contains a BreakStatement at the top level or inside
 * IF/CASE branches. Does NOT descend into nested DO bodies — a BREAK there
 * exits the inner loop, not the outer one.
 * @param {object[]} stmts
 * @returns {boolean}
 */
function hasBreakInBody(stmts) {
    for (const stmt of stmts) {
        if (stmt.type === NodeType.BreakStatement) return true;
        if (stmt.type === NodeType.IfStatement) {
            if (stmt.consequent && hasBreakInBody(stmt.consequent)) return true;
            if (stmt.alternate  && hasBreakInBody(stmt.alternate))  return true;
        } else if (stmt.type === NodeType.CaseStatement) {
            for (const branch of stmt.branches || []) {
                if (branch.body && hasBreakInBody(branch.body)) return true;
            }
            if (stmt.otherwise && hasBreakInBody(stmt.otherwise)) return true;
        }
        // Deliberately do not recurse into nested DoStatement bodies.
    }
    return false;
}

/**
 * Walk a statement list collecting iterator nodes that have BY but no TO.
 * Uses index-aware iteration so it can inspect the next sibling to detect
 * UNTIL closure (EndDoStatement with condition !== null).
 * @param {object[]} stmts
 * @param {Array<{node: object, closedByUntil: boolean}>} out
 */
function collectByOnlyIterators(stmts, out = []) {
    walkStatements(stmts, (stmt, i, siblings) => {
        if (stmt.type === NodeType.DoStatement && stmt.kind === "iterator") {
            const { iterator } = stmt;
            if (iterator.by !== null && iterator.to === null) {
                // Check whether the next sibling closes this loop with UNTIL.
                const next = siblings[i + 1];
                const closedByUntil = !!(
                    next &&
                    next.type === NodeType.EndDoStatement &&
                    next.condition !== null
                );
                out.push({ node: stmt, closedByUntil });
            }
        }
    });
    return out;
}

/**
 * Validates DO iterator loops in a document.
 * Emits ACU-SYN-004 when a DO iterator has no TO clause and no BY clause.
 * Emits ACU-SYN-005 when a DO iterator has BY but no TO and no BREAK, and
 *   is not closed by a UNTIL condition.
 * Emits ACU-SYN-009 when UNTIL appears with no condition expression.
 * @param {object} document
 * @param {Function} getRuleSeverity
 * @returns {object[]}
 */
function validateDoIterators(document, getRuleSeverity, options = {}) {
    const diagnostics = [];
    const { ast } = buildDocumentAst(document);

    // ACU-SYN-004: iterator with neither TO nor BY
    const iterators = [];
    collectIteratorNodes(ast.statements, iterators);

    for (const node of iterators) {
        const { iterator } = node;
        if (iterator.to === null && iterator.by === null) {
            // Range covers the DO keyword. Use the stored char offset (HASH-ON aware).
            const kwChar = typeof node.keywordChar === "number" ? node.keywordChar : 0;
            const range = {
                start: { line: node.line, character: kwChar },
                end:   { line: node.line, character: kwChar + 2 }
            };
            const diag = createDiagnostic(
                range,
                { messageId: "ACU-SYN-004", params: { variable: iterator.variable } },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
        }
    }

    // ACU-SYN-005: iterator with BY but no TO, no BREAK, and not UNTIL-closed
    const byOnlyLoops = collectByOnlyIterators(ast.statements);
    for (const { node, closedByUntil } of byOnlyLoops) {
        if (closedByUntil) continue;
        if (hasBreakInBody(node.body || [])) continue;
        const kwChar = typeof node.keywordChar === "number" ? node.keywordChar : 0;
        const range = {
            start: { line: node.line, character: kwChar },
            end:   { line: node.line, character: kwChar + 2 }
        };
        const diag = createDiagnostic(
            range,
            { messageId: "ACU-SYN-005", params: { variable: node.iterator.variable } },
            getRuleSeverity
        );
        if (diag) diagnostics.push(diag);
    }

    // ACU-SYN-009: UNTIL with no condition expression
    walkStatements(ast.statements, (stmt) => {
        if (stmt.type === NodeType.EndDoStatement && stmt.keyword === "UNTIL" && stmt.condition === null) {
            const lineText = document.lineAt(stmt.line).text;
            const untilPos = lineText.toUpperCase().indexOf("UNTIL");
            const charStart = untilPos >= 0 ? untilPos : 0;
            const range = {
                start: { line: stmt.line, character: charStart },
                end:   { line: stmt.line, character: charStart + 5 }
            };
            const diag = createDiagnostic(
                range,
                { messageId: "ACU-SYN-009", params: {} },
                getRuleSeverity
            );
            if (diag) diagnostics.push(diag);
        }
    });

    return diagnostics;
}

export { validateDoIterators };
