"""Special block parsing for Kida parser.

Provides mixin for parsing special blocks (with, raw, capture, cache, filter_block).

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.environment.exceptions import ErrorCode
from kida.nodes import (
    Block,
    Cache,
    Capture,
    Const,
    Embed,
    Filter,
    FilterBlock,
    Flush,
    Raw,
    Spaceless,
    With,
    WithConditional,
)

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

from kida.parser.blocks.core import BlockStackMixin


class SpecialBlockParsingMixin(BlockStackMixin):
    """Mixin for parsing special blocks.

    Host attributes and cross-mixin dependencies are declared via inline
    TYPE_CHECKING blocks. Inherits block stack management from BlockStackMixin.

    """

    # ─────────────────────────────────────────────────────────────────────────
    # 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]]

        # 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,
            code: ErrorCode | None = None,
        ) -> ParseError: ...

        # From StatementParsingMixin
        def _parse_body(self, stop_on_continuation: bool = False) -> list[Node]: ...
        def _parse_tuple_or_expression(self) -> Expr: ...
        def _skip_comment(self) -> None: ...

        # From ExpressionParsingMixin
        def _parse_expression(self) -> Expr: ...
        def _parse_call_args(self) -> tuple[list[Expr], dict[str, Expr]]: ...

        # From TemplateStructureBlockParsingMixin
        def _parse_block_tag(self) -> Block: ...

    def _parse_with(self) -> Node:
        """Parse {% with %} in two forms:

        1. Conditional: {% with expr %} or {% with expr as name %}
           - Binds expr to 'it' or 'name'
           - Skips body if expr is falsy (nil-resilient)

        2. Assignment: {% with name = expr, ... %}
           - Creates variable bindings
           - Always renders body

        Detection: If first token after 'with' is a NAME followed by '=',
        it's assignment style. Otherwise it's conditional (expression
        followed by 'as' or '%' }).
        """
        start = self._advance()  # consume 'with'
        self._push_block("with", start)

        # Detect style: peek ahead to see if this is NAME = expr or expr [as name]
        if self._is_assignment_style_with():
            return self._parse_assignment_with(start)
        else:
            return self._parse_conditional_with(start)

    def _is_assignment_style_with(self) -> bool:
        """Detect assignment-style: {% with name = expr %}.

        Returns True if current token is NAME and next token is ASSIGN.
        """
        if self._current.type != TokenType.NAME:
            return False

        # Peek ahead to see if next token is '='
        next_tok = self._peek(1)
        return bool(next_tok.type == TokenType.ASSIGN)

    def _parse_assignment_with(self, start: Token) -> With:
        """Parse assignment-style: {% with x = expr, y = expr2 %}...{% end %}.

        Always renders body with the specified variable bindings.
        """
        # Parse variable assignments
        assignments: list[tuple[str, Expr]] = []

        while True:
            if self._current.type != TokenType.NAME:
                raise self._error("Expected variable name")
            name = self._advance().value

            self._expect(TokenType.ASSIGN)
            value = self._parse_expression()

            assignments.append((name, value))

            if not self._match(TokenType.COMMA):
                break
            self._advance()  # consume comma

        self._expect(TokenType.BLOCK_END)

        # Parse body
        body = self._parse_body()

        # Consume end tag
        self._consume_end_tag("with")

        return With(
            lineno=start.lineno,
            col_offset=start.col_offset,
            targets=tuple(assignments),
            body=tuple(body),
        )

    def _parse_conditional_with(self, start: Token) -> WithConditional:
        """Parse conditional: {% with expr %} or {% with expr as name %}...{% end %}.

        Renders body only if expr is truthy. Binds expr to 'name' or 'it'.
        Supports multiple expressions and bindings: {% with a, b as x, y %}.
        Also supports {% else %} or {% empty %} for falsy cases.
        """
        from kida.nodes import Name as KidaName

        # Parse the expression(s) - support implicit tuples
        expr = self._parse_tuple_or_expression()

        # Check for 'as name'
        if self._current.type == TokenType.NAME and self._current.value == "as":
            self._advance()  # consume 'as'
            # Parse targets - support implicit tuples
            target = self._parse_tuple_or_expression()
        else:
            # Default binding name
            target = KidaName(
                lineno=expr.lineno,
                col_offset=expr.col_offset,
                name="it",
                ctx="store",
            )

        self._expect(TokenType.BLOCK_END)

        # Parse body, stopping on continuation (else/empty) or end keywords
        body = self._parse_body(stop_on_continuation=True)

        empty: list[Node] = []

        # Check for continuation keywords
        if self._current.type == TokenType.BLOCK_BEGIN:
            next_tok = self._peek(1)
            if next_tok.type == TokenType.NAME and next_tok.value in ("else", "empty"):
                self._advance()  # consume {%
                self._advance()  # consume 'else' or 'empty'
                self._expect(TokenType.BLOCK_END)
                # After else/empty, only stop on end keywords
                empty = self._parse_body(stop_on_continuation=False)

        # Consume end tag
        self._consume_end_tag("with")

        return WithConditional(
            lineno=start.lineno,
            col_offset=start.col_offset,
            expr=expr,
            target=target,
            body=tuple(body),
            empty=tuple(empty),
        )

    def _parse_flush(self) -> Flush:
        """Parse {% flush %} — streaming flush boundary.

        Creates an explicit yield boundary in streaming mode.
        No-op in non-streaming (render) mode.
        """
        start = self._advance()  # consume 'flush'
        self._expect(TokenType.BLOCK_END)
        return Flush(
            lineno=start.lineno,
            col_offset=start.col_offset,
        )

    def _parse_raw(self) -> Raw:
        """Parse {% raw %}...{% endraw %.

        Raw block that prevents template processing of its content.
        """
        start = self._advance()  # consume 'raw'
        self._expect(TokenType.BLOCK_END)

        # Collect all content until {% endraw %}
        content_parts: list[str] = []

        while True:
            if self._current.type == TokenType.EOF:
                raise self._error("Unclosed raw block", token=start)

            if self._current.type == TokenType.BLOCK_BEGIN:
                # Peek ahead to see if this is {% endraw %}
                self._advance()  # consume {%
                if self._current.type == TokenType.NAME and self._current.value == "endraw":
                    self._advance()  # consume 'endraw'
                    self._expect(TokenType.BLOCK_END)
                    break
                else:
                    # Not endraw, include the block as literal text
                    # We already consumed BLOCK_BEGIN and are at NAME token
                    # Build: {% name %} or {% name args %}
                    content_parts.append("{%")
                    # Collect tokens until BLOCK_END
                    while (
                        self._current.type != TokenType.BLOCK_END
                        and self._current.type != TokenType.EOF
                    ):
                        if self._current.value:
                            content_parts.append(" ")
                            content_parts.append(str(self._current.value))
                        self._advance()
                    if self._current.type == TokenType.BLOCK_END:
                        content_parts.append(" %}")
                        self._advance()
            elif self._current.type == TokenType.DATA:
                content_parts.append(self._current.value)
                self._advance()
            elif self._current.type == TokenType.VARIABLE_BEGIN:
                content_parts.append("{{")
                self._advance()
                # Capture expression tokens until VARIABLE_END
                while (
                    self._current.type != TokenType.VARIABLE_END
                    and self._current.type != TokenType.EOF
                ):
                    if self._current.value:
                        content_parts.append(" ")
                        content_parts.append(str(self._current.value))
                    self._advance()
                content_parts.append(" }}")
                if self._current.type == TokenType.VARIABLE_END:
                    self._advance()
            elif self._current.type == TokenType.BLOCK_END:
                content_parts.append("%}")
                self._advance()
            elif self._current.type == TokenType.COMMENT_BEGIN:
                content_parts.append("{#")
                self._advance()
            elif self._current.type == TokenType.COMMENT_END:
                content_parts.append(" #}")
                self._advance()
            else:
                # Include token value as literal
                if self._current.value:
                    content_parts.append(str(self._current.value))
                self._advance()

        return Raw(
            lineno=start.lineno,
            col_offset=start.col_offset,
            value="".join(content_parts),
        )

    def _parse_capture(self) -> Capture:
        """Parse {% capture name %}...{% end %} or {% endcapture %.

        Capture rendered content into a variable.

        Example:
            {% capture sidebar %}
                <nav>{{ build_nav() }}</nav>
            {% end %}

            {{ sidebar }}
        """
        start = self._advance()  # consume 'capture'
        self._push_block("capture", start)

        # Get variable name
        if self._current.type != TokenType.NAME:
            raise self._error(
                "Expected variable name",
                suggestion="Capture syntax: {% capture varname %}...{% end %}",
            )
        name = self._advance().value

        # Optional filter
        filter_node: Filter | None = None
        if self._match(TokenType.PIPE):
            self._advance()
            if self._current.type != TokenType.NAME:
                raise self._error("Expected filter name")
            filter_name = self._advance().value

            # Optional filter arguments
            filter_args: list[Expr] = []
            filter_kwargs: dict[str, Expr] = {}
            if self._match(TokenType.LPAREN):
                self._advance()
                filter_args, filter_kwargs = self._parse_call_args()
                self._expect(TokenType.RPAREN)

            # Create a placeholder Filter node (value will be the captured content)
            filter_node = Filter(
                lineno=start.lineno,
                col_offset=start.col_offset,
                value=Const(lineno=start.lineno, col_offset=start.col_offset, value=""),
                name=filter_name,
                args=tuple(filter_args),
                kwargs=filter_kwargs,
            )

        self._expect(TokenType.BLOCK_END)

        # Parse body using universal end detection
        body = self._parse_body()

        # Consume end tag
        self._consume_end_tag("capture")

        return Capture(
            lineno=start.lineno,
            col_offset=start.col_offset,
            name=name,
            body=tuple(body),
            filter=filter_node,
        )

    def _parse_cache(self) -> Cache:
        """Parse {% cache key %}...{% end %} or {% endcache %.

        Fragment caching with optional TTL.

        Example:
            {% cache "sidebar-" ~ site.nav_version %}
                {{ build_nav_tree(site.pages) }}
            {% end %}

            {% cache "weather", ttl="5m" %}
                {{ fetch_weather() }}
            {% end %}
        """
        start = self._advance()  # consume 'cache'
        self._push_block("cache", start)

        # Parse cache key expression
        key = self._parse_expression()

        # Optional TTL and depends
        ttl: Expr | None = None
        depends: list[Expr] = []

        while self._match(TokenType.COMMA):
            self._advance()
            if self._current.type == TokenType.NAME:
                if self._current.value == "ttl" and self._peek(1).type == TokenType.ASSIGN:
                    self._advance()  # consume 'ttl'
                    self._advance()  # consume '='
                    ttl = self._parse_expression()
                elif self._current.value == "depends" and self._peek(1).type == TokenType.ASSIGN:
                    self._advance()  # consume 'depends'
                    self._advance()  # consume '='
                    depends.append(self._parse_expression())
                else:
                    break
            else:
                break

        self._expect(TokenType.BLOCK_END)

        # Parse body using universal end detection
        body = self._parse_body()

        # Consume end tag
        self._consume_end_tag("cache")

        return Cache(
            lineno=start.lineno,
            col_offset=start.col_offset,
            key=key,
            body=tuple(body),
            ttl=ttl,
            depends=tuple(depends),
        )

    def _parse_filter_block(self) -> FilterBlock:
        """Parse {% filter name %}...{% end %} or {% endfilter %.

        Apply a filter to an entire block of content.

        Example:
            {% filter upper %}
                This will be UPPERCASE
            {% end %}
        """
        start = self._advance()  # consume 'filter'
        self._push_block("filter", start)

        # Get filter name
        if self._current.type != TokenType.NAME:
            raise self._error(
                "Expected filter name",
                suggestion="Filter block syntax: {% filter name %}...{% end %}",
            )
        filter_name = self._advance().value

        # Optional filter arguments
        filter_args: list[Expr] = []
        filter_kwargs: dict[str, Expr] = {}
        if self._match(TokenType.LPAREN):
            self._advance()
            filter_args, filter_kwargs = self._parse_call_args()
            self._expect(TokenType.RPAREN)

        self._expect(TokenType.BLOCK_END)

        # Parse body using universal end detection
        body = self._parse_body()

        # Consume end tag
        self._consume_end_tag("filter")

        # Create Filter node for the block
        filter_node = Filter(
            lineno=start.lineno,
            col_offset=start.col_offset,
            value=Const(lineno=start.lineno, col_offset=start.col_offset, value=""),
            name=filter_name,
            args=tuple(filter_args),
            kwargs=filter_kwargs,
        )

        return FilterBlock(
            lineno=start.lineno,
            col_offset=start.col_offset,
            filter=filter_node,
            body=tuple(body),
        )

    def _parse_spaceless(self) -> Spaceless:
        """Parse {% spaceless %}...{% end %} or {% endspaceless %}.

        Removes whitespace between HTML tags.
        Part of RFC: kida-modern-syntax-features.

        Example:
            {% spaceless %}
            <ul>
                <li>a</li>
            </ul>
            {% end %}
            Output: <ul><li>a</li></ul>
        """
        start = self._advance()  # consume 'spaceless'
        self._push_block("spaceless", start)

        self._expect(TokenType.BLOCK_END)

        # Parse body using universal end detection
        body = self._parse_body()

        # Consume end tag
        self._consume_end_tag("spaceless")

        return Spaceless(
            lineno=start.lineno,
            col_offset=start.col_offset,
            body=tuple(body),
        )

    def _parse_embed(self) -> Embed:
        """Parse {% embed 'template.html' %}...{% end %} or {% endembed %}.

        Embed is like include but allows block overrides.
        Part of RFC: kida-modern-syntax-features.

        Example:
            {% embed 'card.html' %}
                {% block title %}Custom Title{% end %}
                {% block body %}Custom Content{% end %}
            {% end %}
        """
        start = self._advance()  # consume 'embed'
        self._push_block("embed", start)

        # Parse template path expression
        template = self._parse_expression()

        # Check for 'with context' or 'without context'
        with_context = True
        if self._current.type == TokenType.NAME:
            if self._current.value == "without":
                self._advance()
                if self._current.type == TokenType.NAME and self._current.value == "context":
                    self._advance()
                    with_context = False
                else:
                    raise self._error(
                        "Expected 'context' after 'without'",
                        suggestion="Use 'without context' to not pass context to embedded template",
                    )
            elif self._current.value == "with":
                self._advance()
                if self._current.type == TokenType.NAME and self._current.value == "context":
                    self._advance()
                    with_context = True
                else:
                    raise self._error(
                        "Expected 'context' after 'with'",
                        suggestion="Use 'with context' to pass context to embedded template",
                    )

        self._expect(TokenType.BLOCK_END)

        # Parse block overrides
        blocks: dict[str, Block] = {}

        while self._current.type != TokenType.EOF:
            # Skip DATA tokens (whitespace/text between blocks)
            if self._current.type == TokenType.DATA:
                self._advance()
                continue

            # Skip comments
            if self._current.type == TokenType.COMMENT_BEGIN:
                self._skip_comment()
                continue

            if self._current.type != TokenType.BLOCK_BEGIN:
                break

            next_tok = self._peek(1)
            if next_tok.type != TokenType.NAME:
                break

            keyword = next_tok.value

            if keyword == "block":
                self._advance()  # consume {%
                block = self._parse_block_tag()
                blocks[block.name] = block
            elif keyword in ("end", "endembed"):
                self._consume_end_tag("embed")
                break
            else:
                raise self._error(
                    f"Expected 'block' or 'end' in embed, got '{keyword}'",
                    suggestion="Embed blocks can only contain {% block %} overrides",
                )

        return Embed(
            lineno=start.lineno,
            col_offset=start.col_offset,
            template=template,
            blocks=blocks,
            with_context=with_context,
        )
