import { Token, TokenType, TokenizerError } from './tokenizer';

/**
 * AST Node types for EFTL
 */
export interface AstNode {
    type: string;
    line: number;
    column: number;
    children?: AstNode[];
}

export interface ParserError {
    message: string;
    line: number;
    column: number;
    length: number;
}

type IfPhase = 'condition' | 'then' | 'branches' | 'afterElse';

interface IfContext {
    token: Token;
    phase: IfPhase;
}

/**
 * EFTL Parser - validates structure and reports errors
 */
export class EftlParser {
    private tokens: Token[];
    private pos: number = 0;
    private errors: ParserError[] = [];
    private blockStack: { type: TokenType; token: Token }[] = [];
    private ifContexts: IfContext[] = [];
    private eftlRootCount: number = 0;

    constructor(tokens: Token[]) {
        this.tokens = tokens;
    }

    parse(): { errors: ParserError[] } {
        this.pos = 0;
        this.errors = [];
        this.blockStack = [];
        this.ifContexts = [];
        this.eftlRootCount = 0;

        while (!this.isAtEnd()) {
            this.parseTopLevel();
        }

        // Check for unclosed blocks
        for (const block of this.blockStack) {
            const expectedClose = this.getMatchingClose(block.type);
            this.errors.push({
                message: `Unclosed tag: ${block.token.value} (expected ${expectedClose})`,
                line: block.token.line,
                column: block.token.column,
                length: block.token.length
            });
        }

        if (this.eftlRootCount === 0) {
            const eof = this.current();
            this.errors.push({
                message: 'Missing required [EFTL] root block',
                line: eof.line,
                column: eof.column,
                length: eof.length
            });
        }

        return { errors: this.errors };
    }

    private parseTopLevel(): void {
        const token = this.current();
        this.validateOutsideEftl(token);
        this.validateDirectIfContent(token);

        switch (token.type) {
            case TokenType.EFTL_OPEN:
                if (this.blockStack.some(block => block.type === TokenType.EFTL_OPEN)) {
                    this.addError(token, 'EFTL root blocks cannot be nested');
                } else if (this.eftlRootCount > 0) {
                    this.addError(token, 'Only one [EFTL] root block is allowed');
                }
                this.eftlRootCount++;
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.EFTL_CLOSE:
                this.popBlock(TokenType.EFTL_OPEN, token);
                this.advance();
                break;

            case TokenType.VAR_OPEN:
                if (this.blockStack.some(block => block.type === TokenType.VAR_OPEN)) {
                    this.errors.push({
                        message: 'VAR tags cannot be nested inside another VAR tag',
                        line: token.line,
                        column: token.column,
                        length: token.length
                    });
                }
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.VAR_CLOSE:
                this.popBlock(TokenType.VAR_OPEN, token);
                this.advance();
                break;

            case TokenType.IF_OPEN:
                this.ifContexts.push({ token, phase: 'condition' });
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.IF_CLOSE:
                const closesIf = this.blockStack[this.blockStack.length - 1]?.type === TokenType.IF_OPEN;
                this.popBlock(TokenType.IF_OPEN, token);
                if (closesIf) {
                    this.ifContexts.pop();
                }
                this.advance();
                break;

            case TokenType.CONDITION_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.CONDITION_CLOSE:
                this.popBlock(TokenType.CONDITION_OPEN, token);
                this.advance();
                break;

            case TokenType.THEN_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.THEN_CLOSE:
                this.popBlock(TokenType.THEN_OPEN, token);
                this.advance();
                break;

            case TokenType.ELSE_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.ELSE_CLOSE:
                this.popBlock(TokenType.ELSE_OPEN, token);
                this.advance();
                break;

            case TokenType.ELSE_IF_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.ELSE_IF_CLOSE:
                this.popBlock(TokenType.ELSE_IF_OPEN, token);
                this.advance();
                break;

            case TokenType.WHILE_OPEN:
                if (!/^\[WHILE(?:\s+threshold\s*=\s*"[^"]*")?\s*\]$/.test(token.value)) {
                    this.addError(token, 'WHILE accepts only the optional string attribute threshold="..."');
                }
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.WHILE_CLOSE:
                this.popBlock(TokenType.WHILE_OPEN, token);
                this.advance();
                break;

            case TokenType.DO_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.DO_CLOSE:
                this.popBlock(TokenType.DO_OPEN, token);
                this.advance();
                break;

            case TokenType.TAG_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.TAG_CLOSE:
                this.popBlock(TokenType.TAG_OPEN, token);
                this.advance();
                break;

            case TokenType.SPLIT_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.SPLIT_CLOSE:
                this.popBlock(TokenType.SPLIT_OPEN, token);
                this.advance();
                break;

            case TokenType.TRIM_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.TRIM_CLOSE:
                this.popBlock(TokenType.TRIM_OPEN, token);
                this.advance();
                break;

            case TokenType.CONTAINS_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.CONTAINS_CLOSE:
                this.popBlock(TokenType.CONTAINS_OPEN, token);
                this.advance();
                break;

            case TokenType.FORMAT_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.FORMAT_CLOSE:
                this.popBlock(TokenType.FORMAT_OPEN, token);
                this.advance();
                break;

            case TokenType.EXPR_OPEN:
            case TokenType.EXPR_OUTPUT_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.EXPR_CLOSE:
                // Can close either EXPR_OPEN or EXPR_OUTPUT_OPEN
                const exprBlock = this.blockStack.pop();
                if (!exprBlock || (exprBlock.type !== TokenType.EXPR_OPEN && exprBlock.type !== TokenType.EXPR_OUTPUT_OPEN)) {
                    this.errors.push({
                        message: `Unexpected closing: ${token.value}`,
                        line: token.line,
                        column: token.column,
                        length: token.length
                    });
                    if (exprBlock) {
                        this.blockStack.push(exprBlock);
                    }
                }
                this.advance();
                break;

            case TokenType.COMMENT_OPEN:
                this.pushBlock(token);
                this.advance();
                break;

            case TokenType.COMMENT_CLOSE:
                this.popBlock(TokenType.COMMENT_OPEN, token);
                this.advance();
                break;

            // Self-closing tags - no block needed
            case TokenType.VALUE_OF:
            case TokenType.SIZE_OF:
            case TokenType.HEADER:
                if (!token.value.endsWith('/]')) {
                    this.pushBlock(token);
                }
                this.advance();
                break;

            case TokenType.TEXT:
            case TokenType.EOF:
                this.advance();
                break;

            default:
                // Unknown token, skip
                this.advance();
                break;
        }
    }

    private validateOutsideEftl(token: Token): void {
        if (this.blockStack.some(block => block.type === TokenType.EFTL_OPEN)) {
            return;
        }

        if (token.type === TokenType.EFTL_OPEN || token.type === TokenType.EFTL_CLOSE ||
            token.type === TokenType.COMMENT_OPEN || token.type === TokenType.COMMENT_CLOSE ||
            token.type === TokenType.EOF || (token.type === TokenType.TEXT && token.value.trim() === '')) {
            return;
        }

        this.addError(token, 'Only comments and whitespace are allowed outside the [EFTL] root block');
    }

    private validateDirectIfContent(token: Token): void {
        const parent = this.blockStack[this.blockStack.length - 1];
        if (!parent || parent.type !== TokenType.IF_OPEN) {
            return;
        }

        const context = this.ifContexts[this.ifContexts.length - 1];
        if (!context || context.token !== parent.token) {
            return;
        }

        if (token.type === TokenType.TEXT && token.value.trim() === '') {
            return;
        }

        if (token.type === TokenType.IF_OPEN) {
            this.addError(token, 'IF cannot be nested directly inside another IF; place it inside a branch block');
            return;
        }

        if (context.phase === 'condition' && token.type === TokenType.CONDITION_OPEN) {
            context.phase = 'then';
            return;
        }

        if (context.phase === 'then' && token.type === TokenType.THEN_OPEN) {
            context.phase = 'branches';
            return;
        }

        if (context.phase === 'branches' && token.type === TokenType.ELSE_IF_OPEN) {
            return;
        }

        if (context.phase === 'branches' && token.type === TokenType.ELSE_OPEN) {
            context.phase = 'afterElse';
            return;
        }

        if (token.type === TokenType.IF_CLOSE) {
            if (context.phase === 'condition') {
                this.addError(token, 'Expected [CONDITION] immediately after [IF]');
            } else if (context.phase === 'then') {
                this.addError(token, 'Expected [THEN] immediately after [/CONDITION]');
            }
            return;
        }

        const expected = context.phase === 'condition'
            ? '[CONDITION]'
            : context.phase === 'then'
                ? '[THEN]'
                : context.phase === 'branches'
                    ? '[ELSE IF], [ELSE], or [/IF]'
                    : '[/IF]';
        this.addError(token, `Expected ${expected} as the next direct child of [IF], got ${token.value}`);
    }

    private addError(token: Token, message: string): void {
        this.errors.push({
            message,
            line: token.line,
            column: token.column,
            length: token.length
        });
    }

    private pushBlock(token: Token): void {
        if (token.value.endsWith('/]')) {
            return; // It's self-closing, don't push to stack
        }
        this.blockStack.push({ type: token.type, token });
    }

    private popBlock(expectedOpenType: TokenType, closeToken: Token): void {
        if (this.blockStack.length === 0) {
            this.errors.push({
                message: `Unexpected closing tag: ${closeToken.value} (no matching opening tag)`,
                line: closeToken.line,
                column: closeToken.column,
                length: closeToken.length
            });
            return;
        }

        const top = this.blockStack[this.blockStack.length - 1];
        if (top.type !== expectedOpenType) {
            // Find the expected closing tag name
            const expectedClose = this.getMatchingClose(top.type);
            this.errors.push({
                message: `Mismatched closing tag: expected ${expectedClose}, got ${closeToken.value}`,
                line: closeToken.line,
                column: closeToken.column,
                length: closeToken.length
            });
            return;
        }

        this.blockStack.pop();
    }

    private getMatchingClose(openType: TokenType): string {
        const closeMap: { [key: string]: string } = {
            [TokenType.EFTL_OPEN]: '[/EFTL]',
            [TokenType.VAR_OPEN]: '[/VAR]',
            [TokenType.IF_OPEN]: '[/IF]',
            [TokenType.CONDITION_OPEN]: '[/CONDITION]',
            [TokenType.THEN_OPEN]: '[/THEN]',
            [TokenType.ELSE_OPEN]: '[/ELSE]',
            [TokenType.ELSE_IF_OPEN]: '[/ELSE IF]',
            [TokenType.WHILE_OPEN]: '[/WHILE]',
            [TokenType.DO_OPEN]: '[/DO]',
            [TokenType.TAG_OPEN]: '[/TAG]',
            [TokenType.SPLIT_OPEN]: '[/SPLIT]',
            [TokenType.TRIM_OPEN]: '[/TRIM]',
            [TokenType.CONTAINS_OPEN]: '[/CONTAINS]',
            [TokenType.FORMAT_OPEN]: '[/FORMAT]',
            [TokenType.VALUE_OF]: '[/VALUE_OF]',
            [TokenType.SIZE_OF]: '[/SIZE_OF]',
            [TokenType.HEADER]: '[/HEADER]',
            [TokenType.EXPR_OPEN]: '%]',
            [TokenType.EXPR_OUTPUT_OPEN]: '%]',
            [TokenType.COMMENT_OPEN]: '--]'
        };
        return closeMap[openType] || 'unknown';
    }

    private current(): Token {
        return this.tokens[this.pos] || { type: TokenType.EOF, value: '', line: 0, column: 0, length: 0 };
    }

    private advance(): Token {
        if (!this.isAtEnd()) {
            this.pos++;
        }
        return this.tokens[this.pos - 1];
    }

    private isAtEnd(): boolean {
        return this.current().type === TokenType.EOF;
    }
}
