"""Statement parsing for Kida parser.

Provides mixin for parsing template statements (body, data, output, blocks).

Uses inline TYPE_CHECKING declarations for host attributes.
See: plan/rfc-mixin-protocol-typing.md
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import TYPE_CHECKING

from kida._types import Token, TokenType
from kida.nodes import Data, Output

if TYPE_CHECKING:
    from kida.nodes import Expr, Node
    from kida.parser.errors import ParseError


# =============================================================================
# Block Keyword Dispatch Table
# =============================================================================
# Maps block keywords to their parser method names.
# Reduces cyclomatic complexity from 40+ elif branches to O(1) dict lookup.
# See RFC: rfc-code-smell-remediation.md §1.1
# =============================================================================

_BLOCK_PARSERS: dict[str, str] = {
    # Control flow
    "if": "_parse_if",
    "unless": "_parse_unless",  # RFC: kida-modern-syntax-features
    "for": "_parse_for",
    "async": "_parse_async",  # RFC: rfc-async-rendering (two-token lookahead)
    "while": "_parse_while",  # RFC: kida-2.0-moonshot
    "break": "_parse_break",  # RFC: kida-modern-syntax-features
    "continue": "_parse_continue",  # RFC: kida-modern-syntax-features
    # Variables
    "set": "_parse_set",
    "let": "_parse_let",
    "export": "_parse_export",
    # Template structure
    "block": "_parse_block_tag",
    "fragment": "_parse_fragment_tag",
    "globals": "_parse_globals_tag",
    "imports": "_parse_imports_tag",
    "extends": "_parse_extends",
    "include": "_parse_include",
    "import": "_parse_import",
    "from": "_parse_from_import",
    # Scope and execution
    "with": "_parse_with",
    "raw": "_parse_raw",
    "def": "_parse_def",
    "region": "_parse_region",
    "call": "_parse_call",
    "capture": "_parse_capture",
    "cache": "_parse_cache",
    "filter": "_parse_filter_block",
    "flush": "_parse_flush",
    # Advanced features
    "slot": "_parse_slot",
    "match": "_parse_match",
    "spaceless": "_parse_spaceless",  # RFC: kida-modern-syntax-features
    "embed": "_parse_embed",  # RFC: kida-modern-syntax-features
}

# Continuation keywords that are invalid outside their block context
_CONTINUATION_KEYWORDS: frozenset[str] = frozenset({"elif", "else", "empty", "case"})

# End keywords that close blocks
_END_KEYWORDS: frozenset[str] = frozenset(
    {
        "endif",
        "endfor",
        "endblock",
        "endwith",
        "endraw",
        "end",
        "enddef",
        "endregion",
        "endcall",
        "endcapture",
        "endcache",
        "endfilter",
        "endmatch",
        "endspaceless",
        "endembed",
        "endfragment",
        "endglobals",
        "endimports",
    }
)

# All valid block keywords for error messages
_VALID_KEYWORDS: frozenset[str] = frozenset(_BLOCK_PARSERS.keys())


class StatementParsingMixin:
    """Mixin for parsing template statements.

    Host attributes and cross-mixin dependencies are declared via inline
    TYPE_CHECKING blocks.

    """

    # ─────────────────────────────────────────────────────────────────────────
    # Host attributes and cross-mixin dependencies (type-check only)
    # ─────────────────────────────────────────────────────────────────────────
    if TYPE_CHECKING:
        # Host attributes (from Parser.__init__)
        _tokens: Sequence[Token]
        _pos: int
        _block_stack: list[tuple[str, int, int]]
        _source: str | None
        _filename: str | None
        _autoescape: bool

        # From TokenNavigationMixin (ParserCoreProtocol members)
        @property
        def _current(self) -> Token: ...
        def _peek(self, offset: int = 0) -> Token: ...
        def _advance(self) -> Token: ...
        def _expect(self, token_type: TokenType) -> Token: ...
        def _match(self, *types: TokenType) -> bool: ...
        def _error(
            self,
            message: str,
            token: Token | None = None,
            suggestion: str | None = None,
        ) -> ParseError: ...

        # From ExpressionParsingMixin
        def _parse_expression(self) -> Expr: ...
        def _parse_primary(self) -> Expr: ...
        def _parse_null_coalesce_no_ternary(self) -> Expr: ...

    # Class-level constants (accessed by methods below)
    _END_KEYWORDS: frozenset[str] = _END_KEYWORDS
    _CONTINUATION_KEYWORDS: frozenset[str] = _CONTINUATION_KEYWORDS

    def _parse_body(
        self,
        stop_on_continuation: bool = False,
    ) -> list[Node]:
        """Parse template body until an end tag or EOF.

        Uses universal end detection: stops on ANY end keyword (end, endif,
        endfor, enddef, etc.) or continuation keyword (else, elif, empty)
        if stop_on_continuation is True.

        This enables the unified {% end %} syntax where {% end %} always
        closes the innermost open block.

        Args:
            stop_on_continuation: If True, also stop on else/elif/empty keywords.
                                 Used by if/for blocks that have continuation clauses.

        Returns:
            List of parsed nodes.
        """
        nodes: list[Node] = []

        while self._current.type != TokenType.EOF:
            # Check for block begin that might contain end/continuation keyword
            if self._current.type == TokenType.BLOCK_BEGIN:
                # Peek ahead to see if next token is an end or continuation keyword
                next_tok = self._peek(1)
                if next_tok.type == TokenType.NAME:
                    # Stop on ANY end keyword - this is the key to unified {% end %}
                    if next_tok.value in self._END_KEYWORDS:
                        # Don't consume the BLOCK_BEGIN, let parent handle closing
                        break

                    # Stop on continuation keywords if requested (for if/for blocks)
                    if stop_on_continuation and next_tok.value in self._CONTINUATION_KEYWORDS:
                        break

                result = self._parse_block()
                if result is not None:
                    # Flatten multi-set results (returns list[Node])
                    if isinstance(result, list):
                        nodes.extend(result)
                    else:
                        nodes.append(result)
            elif self._current.type == TokenType.DATA:
                nodes.append(self._parse_data())
            elif self._current.type == TokenType.VARIABLE_BEGIN:
                nodes.append(self._parse_output())
            elif self._current.type == TokenType.COMMENT_BEGIN:
                self._skip_comment()
            else:
                self._advance()

        return nodes

    def _parse_data(self) -> Data:
        """Parse raw text data."""
        token = self._advance()
        return Data(
            lineno=token.lineno,
            col_offset=token.col_offset,
            value=token.value,
        )

    def _parse_output(self) -> Output:
        """Parse {{ expression }}."""
        start = self._expect(TokenType.VARIABLE_BEGIN)
        expr = self._parse_expression()
        self._expect(TokenType.VARIABLE_END)

        return Output(
            lineno=start.lineno,
            col_offset=start.col_offset,
            expr=expr,
            escape=self._autoescape,
        )

    def _parse_block(self) -> Node | list[Node] | None:
        """Parse {% ... %} block tag.

        Returns:
            - Single Node for most blocks
            - list[Node] for multi-set ({% set a = 1, b = 2 %})
            - None for end tags
        """
        self._expect(TokenType.BLOCK_BEGIN)
        return self._parse_block_content()

    def _parse_block_content(self) -> Node | list[Node] | None:
        """Parse block content after BLOCK_BEGIN is consumed.

        This is split from _parse_block so it can be reused in contexts
        where BLOCK_BEGIN is already consumed (e.g., inside function bodies).

        Uses dispatch table pattern for O(1) keyword lookup instead of
        20+ elif branches. See RFC: rfc-code-smell-remediation.md §1.1

        Returns:
            - Single Node for most blocks
            - list[Node] for multi-set ({% set a = 1, b = 2 %})
            - None for end tags
        """
        if self._current.type != TokenType.NAME:
            raise self._error(
                "Expected block keyword (if, for, set, block, etc.)",
                suggestion="Block tags should start with a keyword like {% if %}, {% for %}, {% set %}",
            )

        keyword = self._current.value

        # Fast path: dispatch table lookup for block keywords (O(1))
        parser_name = _BLOCK_PARSERS.get(keyword)
        if parser_name is not None:
            parser = getattr(self, parser_name)
            return parser()

        # Handle continuation keywords (elif, else, empty, case)
        if keyword in _CONTINUATION_KEYWORDS:
            raise self._error(
                f"Unexpected '{keyword}' - not inside a matching block",
                suggestion=f"'{keyword}' can only appear inside an 'if' or 'for' block",
            )

        # Handle end keywords (endif, endfor, end, etc.)
        if keyword in _END_KEYWORDS:
            self._handle_end_keyword(keyword)
            return None

        # Unknown keyword
        raise self._error(
            f"Unknown block keyword: {keyword}",
            suggestion=f"Valid keywords: {', '.join(sorted(_VALID_KEYWORDS))}",
        )

    def _handle_end_keyword(self, keyword: str) -> None:
        """Handle end keywords (endif, endfor, end, etc.).

        Args:
            keyword: The end keyword being processed

        Returns:
            None for valid end tags (signals parent to close block)

        Raises:
            TemplateParseError: For mismatched or orphaned end tags
        """
        # End tag without matching opening block
        if not self._block_stack:
            raise self._error(
                f"Unexpected '{keyword}' - no open block to close",
                suggestion="Remove this tag or add a matching opening tag",
            )

        # Check if end tag matches the innermost block
        innermost_block = self._block_stack[-1][0]
        expected_end = f"end{innermost_block}" if innermost_block != "block" else "endblock"

        if keyword == "end":
            # Unified {% end %} is always valid if there's an open block
            return None
        elif keyword == expected_end:
            # Matching end tag - let parent handle it
            return None
        else:
            # Mismatched end tag
            raise self._error(
                f"Mismatched closing tag: expected '{{% {expected_end} %}}' or '{{% end %}}', got '{{% {keyword} %}}'",
                suggestion=f"The innermost open block is '{innermost_block}' (opened at line {self._block_stack[-1][1]})",
            )

    def _skip_comment(self) -> None:
        """Skip comment block."""
        self._expect(TokenType.COMMENT_BEGIN)
        self._expect(TokenType.COMMENT_END)

    def _get_eof_error_suggestion(self, block_type: str) -> str:
        """Generate improved error suggestion for EOF errors in blocks.

        Checks for unclosed comments and provides helpful suggestions.
        """
        unclosed = self._check_unclosed_comment()
        if unclosed:
            unclosed_line, unclosed_col = unclosed
            return (
                f"Unclosed comment at line {unclosed_line}:{unclosed_col}. "
                f"Add '#}}' to close the comment, "
                f"or add {{% end %}} to close the {block_type} block."
            )
        return f"Add {{% end %}} or {{% end{block_type} %}}"

    def _check_unclosed_comment(self) -> tuple[int, int] | None:
        """Check for unclosed comment in the source.

        Scans the source for {# without matching #}.
        Returns (line, col) of unclosed comment start if found, None otherwise.
        """
        if not self._source:
            return None

        lines = self._source.splitlines()

        comment_start_line = None
        comment_start_col = None
        in_comment = False

        for line_num, line in enumerate(lines, 1):
            pos = 0

            while pos < len(line):
                if not in_comment:
                    # Look for comment start
                    start_pos = line.find("{#", pos)
                    if start_pos == -1:
                        break
                    # Check if comment closes on same line
                    end_pos = line.find("#}", start_pos + 2)
                    if end_pos == -1:
                        # Comment starts but doesn't close - track it
                        in_comment = True
                        comment_start_line = line_num
                        comment_start_col = start_pos
                        pos = start_pos + 2
                    else:
                        # Comment closed on same line, skip past it
                        pos = end_pos + 2
                else:
                    # Inside comment, look for closing
                    end_pos = line.find("#}", pos)
                    if end_pos == -1:
                        # Comment continues to next line
                        break
                    # Comment closes here
                    in_comment = False
                    pos = end_pos + 2

        if in_comment and comment_start_line is not None:
            return (comment_start_line, comment_start_col or 0)

        return None

    # Helper methods for tuple/assignment parsing
    def _parse_tuple_or_name(self) -> Expr:
        """Parse assignment target (variable or tuple for unpacking).

        Used by set/let/export for patterns like:
            - x (single variable)
            - a, b (comma-separated before '=')
            - (a, b) (parenthesized tuple)
            - (a, b), c (nested/mixed)
        """
        from kida.nodes import Tuple

        # Parse first item (name or parenthesized tuple)
        # _parse_primary() handles both cases - names and parenthesized tuples
        first = self._parse_primary()

        # Check for comma (tuple unpacking)
        if self._match(TokenType.COMMA):
            items = [first]
            while self._match(TokenType.COMMA):
                self._advance()
                if self._current.type == TokenType.ASSIGN:
                    break  # trailing comma before '='
                items.append(self._parse_primary())

            return Tuple(
                lineno=first.lineno,
                col_offset=first.col_offset,
                items=tuple(items),
                ctx="store",
            )

        return first

    def _parse_tuple_or_expression(self) -> Expr:
        """Parse expression that may be an implicit tuple.

        Used for value side of set statements like:
            {% set a, b = 1, 2 %}

        The value `1, 2` is parsed as a tuple without parentheses.
        """
        from kida.nodes import Tuple

        first = self._parse_expression()

        # Check for comma (implicit tuple like 1, 2, 3)
        if self._match(TokenType.COMMA):
            items = [first]
            while self._match(TokenType.COMMA):
                self._advance()
                if self._match(TokenType.BLOCK_END):
                    break  # trailing comma before %}
                items.append(self._parse_expression())

            return Tuple(
                lineno=first.lineno,
                col_offset=first.col_offset,
                items=tuple(items),
                ctx="load",
            )

        return first

    def _parse_tuple_or_null_coalesce_no_ternary(self) -> Expr:
        """Parse null coalescing that may be an implicit tuple, without ternary.

        Used for match subject and case patterns to allow:
            {% match a, b %}
            {% case 1, 2 %}
        while preserving the ability to use 'if' as a guard clause:
            {% case 1 if x %}  <- 'if' is a guard, not a ternary conditional
        """
        from kida.nodes import Tuple

        first = self._parse_null_coalesce_no_ternary()

        # Check for comma (implicit tuple like 1, 2, 3)
        if self._match(TokenType.COMMA):
            items = [first]
            while self._match(TokenType.COMMA):
                self._advance()
                if self._match(TokenType.BLOCK_END):
                    break  # trailing comma before %}

                # Stop if we see 'if' - it's a guard clause, not part of the tuple
                if self._current.type == TokenType.NAME and self._current.value == "if":
                    break

                items.append(self._parse_null_coalesce_no_ternary())

            return Tuple(
                lineno=first.lineno,
                col_offset=first.col_offset,
                items=tuple(items),
                ctx="load",
            )

        return first

    def _parse_for_target(self) -> Expr:
        """Parse for loop target (variable or tuple for unpacking).

        Handles:
            - item (single variable)
            - (a, b) (parenthesized tuple)
            - a, b, c (comma-separated names before 'in')
        """
        from kida.nodes import Tuple

        # Check for parenthesized tuple
        if self._match(TokenType.LPAREN):
            return self._parse_primary()  # Will parse as tuple

        # Parse first name
        first = self._parse_primary()

        # Check for comma (tuple unpacking without parens)
        if self._match(TokenType.COMMA):
            items = [first]
            while self._match(TokenType.COMMA):
                self._advance()
                if self._current.type == TokenType.IN:
                    break  # trailing comma before 'in'
                items.append(self._parse_primary())

            return Tuple(
                lineno=first.lineno,
                col_offset=first.col_offset,
                items=tuple(items),
                ctx="store",
            )

        return first
