"""Kida AST node definitions.

Immutable, frozen dataclass nodes representing the Kida template AST.
All nodes track source location (lineno, col_offset) for error reporting.

Kida-Native Features:
    - **Unified endings**: `{% end %}` closes any block (like Go templates)
    - **Functions**: `{% def %}` with true lexical scoping (not Jinja macros)
    - **Pipeline**: `|>` for readable filter chains
    - **Pattern matching**: `{% match %}...{% case %}...{% end %}`
    - **Caching**: `{% cache key %}...{% end %}` with TTL support
    - **Explicit scoping**: `{% let %}` (template), `{% set %}` (block), `{% export %}`

Node Categories:
    **Template Structure**:
        - `Template`: Root node containing body
        - `Extends`, `Block`, `Include`: Inheritance and composition
        - `Import`, `FromImport`: Function imports

    **Control Flow**:
        - `If`, `For`, `While`: Standard control flow
        - `AsyncFor`: Native async iteration
        - `Match`: Pattern matching

    **Variables**:
        - `Set`: Block-scoped assignment
        - `Let`: Template-scoped assignment
        - `Export`: Export from inner scope to enclosing scope
        - `Capture`: Capture block output to variable

    **Functions**:
        - `Def`: Function definition
        - `CallBlock`: Call function with body content
        - `Slot`: Content placeholder in components

    **Expressions**:
        - `Const`, `Name`: Literals and identifiers
        - `Getattr`, `Getitem`: Attribute and subscript access
        - `FuncCall`, `Filter`, `Pipeline`: Function calls and filters
        - `BinOp`, `UnaryOp`, `Compare`, `BoolOp`: Operators
        - `CondExpr`: Ternary conditional
        - `Test`: `is` test expressions

    **Output**:
        - `Output`: Expression output `{{ expr }}`
        - `Data`: Raw text between template constructs

Thread-Safety:
    All nodes are frozen dataclasses, making the AST immutable and safe
    for concurrent access. The Parser produces a new AST on each call.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass, field
from typing import Literal

# =============================================================================
# Base Node
# =============================================================================


@dataclass(frozen=True, slots=True)
class Node:
    """Base class for all AST nodes.

    All nodes track their source location for error reporting.
    Nodes are immutable for thread-safety.
    """

    lineno: int
    col_offset: int


# =============================================================================
# Template Structure
# =============================================================================


@dataclass(frozen=True, slots=True)
class Template(Node):
    """Root node representing a complete template.

    Attributes:
        body: Sequence of top-level nodes
        extends: Optional parent template path
        context_type: Optional type declaration from {% template %}
    """

    body: Sequence[Node]
    extends: Extends | None = None
    context_type: TemplateContext | None = None


@dataclass(frozen=True, slots=True)
class TemplateContext(Node):
    """Type declaration: {% template page: Page, site: Site %}

    Kida-native feature for type-aware validation.
    """

    declarations: Sequence[tuple[str, str]]  # (name, type_name)


@dataclass(frozen=True, slots=True)
class Extends(Node):
    """Template inheritance: {% extends "base.html" %}"""

    template: Expr


@dataclass(frozen=True, slots=True)
class Block(Node):
    """Named block for inheritance: {% block name %}...{% end %}

    Kida uses unified {% end %} for all block closings.

    Attributes:
        name: Block identifier
        body: Block content
        scoped: If True, block has its own variable scope
        required: If True, child templates must override this block
    """

    name: str
    body: Sequence[Node]
    scoped: bool = False
    required: bool = False


@dataclass(frozen=True, slots=True)
class Include(Node):
    """Include another template: {% include "partial.html" %}

    Attributes:
        template: Template path expression
        with_context: If True, pass current context to included template
        ignore_missing: If True, silently skip if template doesn't exist
    """

    template: Expr
    with_context: bool = True
    ignore_missing: bool = False


@dataclass(frozen=True, slots=True)
class Import(Node):
    """Import functions from template: {% import "funcs.html" as f %}"""

    template: Expr
    target: str
    with_context: bool = False


@dataclass(frozen=True, slots=True)
class FromImport(Node):
    """Import specific functions: {% from "funcs.html" import button, card %}"""

    template: Expr
    names: Sequence[tuple[str, str | None]]  # (name, alias)
    with_context: bool = False


# =============================================================================
# Statements
# =============================================================================


@dataclass(frozen=True, slots=True)
class Output(Node):
    """Output expression: {{ expr }}

    Attributes:
        expr: Expression to output
        escape: If True, HTML-escape the result
    """

    expr: Expr
    escape: bool = True


@dataclass(frozen=True, slots=True)
class Data(Node):
    """Raw text data between template constructs."""

    value: str


@dataclass(frozen=True, slots=True)
class If(Node):
    """Conditional: {% if cond %}...{% elif cond %}...{% else %}...{% end %}

    Kida uses unified {% end %} instead of {% endif %}.

    Attributes:
        test: Condition expression
        body: Nodes to render if condition is true
        elif_: Sequence of (condition, body) pairs
        else_: Nodes to render if all conditions are false
    """

    test: Expr
    body: Sequence[Node]
    elif_: Sequence[tuple[Expr, Sequence[Node]]] = ()
    else_: Sequence[Node] = ()


@dataclass(frozen=True, slots=True)
class For(Node):
    """For loop: {% for x in items %}...{% empty %}...{% end %}

    Kida uses {% empty %} (not {% else %}) and {% end %} (not {% endfor %}).

    Attributes:
        target: Loop variable(s) - can be tuple for unpacking
        iter: Iterable expression
        body: Loop body
        empty: Rendered if iterable is empty (Kida uses 'empty' not 'else')
        recursive: Enable recursive loop calls
        test: Optional filter condition (like Python's if in comprehensions)
    """

    target: Expr
    iter: Expr
    body: Sequence[Node]
    empty: Sequence[Node] = ()  # Kida: 'empty' not 'else_'
    recursive: bool = False
    test: Expr | None = None


@dataclass(frozen=True, slots=True)
class AsyncFor(Node):
    """Async for loop: {% async for x in async_items %}...{% end %}

    Native async iteration without wrapper adapters.
    """

    target: Expr
    iter: Expr
    body: Sequence[Node]
    empty: Sequence[Node] = ()


@dataclass(frozen=True, slots=True)
class While(Node):
    """While loop: {% while cond %}...{% end %}

    Kida-native feature.
    """

    test: Expr
    body: Sequence[Node]


@dataclass(frozen=True, slots=True)
class Match(Node):
    """Pattern matching: {% match expr %}{% case pattern [if guard] %}...{% end %}

    Kida-native feature for cleaner branching than if/elif chains.
    Supports optional guard clauses for conditional matching.

    Example:
        {% match page.type %}
            {% case "post" %}<i class="icon-pen"></i>
            {% case "gallery" %}<i class="icon-image"></i>
            {% case _ %}<i class="icon-file"></i>
        {% end %}

    With guards:
        {% match api_type %}
            {% case _ if 'python' in api_type %}Python API
            {% case _ if 'rest' in api_type %}REST API
            {% case _ %}Other
        {% end %}
    """

    subject: Expr
    cases: Sequence[tuple[Expr, Expr | None, Sequence[Node]]]  # (pattern, guard, body)


# =============================================================================
# Variable Statements (Kida's explicit scoping)
# =============================================================================


@dataclass(frozen=True, slots=True)
class Let(Node):
    """Template-scoped variable: {% let x = expr %} or {% let a, b = 1, 2 %}

    Variables declared with 'let' persist across the template
    and can be modified within inner scopes.
    Supports tuple unpacking on the left-hand side.

    Kida-native replacement for Jinja's confusing namespace() workaround.
    """

    name: Expr  # Name or Tuple for unpacking
    value: Expr


@dataclass(frozen=True, slots=True)
class Set(Node):
    """Block-scoped variable: {% set x = expr %} or {% set a, b = 1, 2 %}

    Variable is scoped to current block. Use 'let' for template-wide scope.
    Supports tuple unpacking on the left-hand side.

    Attributes:
        target: Assignment target - can be a Name or Tuple of Names
        value: Value expression to assign
    """

    target: Expr  # Name or Tuple for unpacking
    value: Expr


@dataclass(frozen=True, slots=True)
class Export(Node):
    """Export variable from inner scope: {% export x = expr %} or {% export a, b = 1, 2 %}

    Explicitly exports a variable from an inner scope (like a for loop)
    to the enclosing scope. Makes scope behavior explicit and predictable.
    Supports tuple unpacking on the left-hand side.

    Example:
        {% for item in items %}
            {% export last = item %}
        {% end %}
        {{ last }}
    """

    name: Expr  # Name or Tuple for unpacking
    value: Expr


@dataclass(frozen=True, slots=True)
class Capture(Node):
    """Capture block content: {% capture x %}...{% end %}

    Kida-native name (clearer than Jinja's {% set x %}...{% endset %}).
    """

    name: str
    body: Sequence[Node]
    filter: Filter | None = None


# =============================================================================
# Functions (Kida-native, replaces macros)
# =============================================================================


@dataclass(frozen=True, slots=True)
class Def(Node):
    """Function definition: {% def name(args) %}...{% end %}

    Kida uses functions with true lexical scoping instead of macros.
    Functions can access variables from their enclosing scope.

    Example:
        {% def card(item) %}
            <div>{{ item.title }}</div>
            <span>From: {{ site.title }}</span>  {# Can access outer scope #}
        {% end %}

        {{ card(page) }}

    Attributes:
        name: Function name
        args: Argument names
        body: Function body
        defaults: Default argument values
    """

    name: str
    args: Sequence[str]
    body: Sequence[Node]
    defaults: Sequence[Expr] = ()


@dataclass(frozen=True, slots=True)
class Slot(Node):
    """Slot for component content: {% slot %}

    Used inside {% def %} to mark where caller content goes.

    Example:
        {% def card(title) %}
            <div class="card">
                <h3>{{ title }}</h3>
                <div class="body">{% slot %}</div>
            </div>
        {% end %}

        {% call card("My Title") %}
            <p>This goes in the slot!</p>
        {% end %}
    """

    name: str = "default"


@dataclass(frozen=True, slots=True)
class CallBlock(Node):
    """Call function with body content: {% call name(args) %}body{% end %}

    The body content fills the {% slot %} in the function.
    """

    call: Expr
    body: Sequence[Node]
    args: Sequence[Expr] = ()


# =============================================================================
# Caching (Kida-native)
# =============================================================================


@dataclass(frozen=True, slots=True)
class Cache(Node):
    """Fragment caching: {% cache key %}...{% end %}

    Kida-native built-in caching. No external dependencies required.

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

        {% cache "weather", ttl="5m" %}
            {{ fetch_weather() }}
        {% end %}

    Attributes:
        key: Cache key expression
        body: Content to cache
        ttl: Optional time-to-live expression
        depends: Optional dependency expressions for invalidation
    """

    key: Expr
    body: Sequence[Node]
    ttl: Expr | None = None
    depends: Sequence[Expr] = ()


# =============================================================================
# Misc Statements
# =============================================================================


@dataclass(frozen=True, slots=True)
class With(Node):
    """Jinja2-style context manager: {% with x = expr %}...{% end %}

    Always renders body with variable bindings.
    """

    targets: Sequence[tuple[str, Expr]]
    body: Sequence[Node]


@dataclass(frozen=True, slots=True)
class WithConditional(Node):
    """Conditional with block: {% with expr as name %}...{% end %}

    Renders body only if expr is truthy. Binds the evaluated expression
    to the specified variable name(s).

    This provides nil-resilience: the block is silently skipped when the
    expression evaluates to None, empty collections, or other falsy values.

    Syntax:
        {% with page.author as author %}
            <span>{{ author.name }}</span>
        {% end %}

        {% with page.author %}
            <span>{{ it.name }}</span>
        {% end %}

        {% with a, b as x, y %}
            {{ x }}, {{ y }}
        {% end %}

    Behavior:
        - Evaluates expr once
        - If truthy: binds result to target, renders body
        - If falsy: renders empty block (if provided), or skips entirely
        - Restores previous variable binding after block

    Contrast with standard With node:
        - With: Always renders body with variable bindings
        - WithConditional: Only renders if expression is truthy
    """

    expr: Expr
    target: Expr  # Name or Tuple for binding
    body: Sequence[Node]
    empty: Sequence[Node] = ()


@dataclass(frozen=True, slots=True)
class FilterBlock(Node):
    """Apply filter to block: {% filter upper %}...{% end %}"""

    filter: Filter
    body: Sequence[Node]


@dataclass(frozen=True, slots=True)
class Autoescape(Node):
    """Control autoescaping: {% autoescape true %}...{% end %}"""

    enabled: bool
    body: Sequence[Node]


@dataclass(frozen=True, slots=True)
class Raw(Node):
    """Raw block (no template processing): {% raw %}...{% end %}"""

    value: str


@dataclass(frozen=True, slots=True)
class Trim(Node):
    """Whitespace control block: {% trim %}...{% end %}

    Kida-native replacement for Jinja's {%- -%} modifiers.
    Content inside is trimmed of leading/trailing whitespace.
    """

    body: Sequence[Node]


# =============================================================================
# Modern Syntax Features (RFC: kida-modern-syntax-features)
# =============================================================================


@dataclass(frozen=True, slots=True)
class Break(Node):
    """Break out of loop: {% break %}

    Exits the innermost for/while loop.
    Part of RFC: kida-modern-syntax-features.
    """

    pass


@dataclass(frozen=True, slots=True)
class Continue(Node):
    """Skip to next iteration: {% continue %}

    Skips to the next iteration of the innermost for/while loop.
    Part of RFC: kida-modern-syntax-features.
    """

    pass


@dataclass(frozen=True, slots=True)
class Spaceless(Node):
    """Remove whitespace between HTML tags: {% spaceless %}...{% end %}

    Removes whitespace between > and <, preserving content whitespace.
    Part of RFC: kida-modern-syntax-features.
    """

    body: Sequence[Node]


@dataclass(frozen=True, slots=True)
class Embed(Node):
    """Embed template with block overrides: {% embed 'card.html' %}...{% end %}

    Like include, but allows overriding blocks in the embedded template.
    Part of RFC: kida-modern-syntax-features.

    Attributes:
        template: Template path expression
        blocks: Block overrides defined in embed body
        with_context: Pass current context to embedded template
    """

    template: Expr
    blocks: dict[str, Block]
    with_context: bool = True


# =============================================================================
# Expressions
# =============================================================================


@dataclass(frozen=True, slots=True)
class Expr(Node):
    """Base class for expressions."""

    pass


@dataclass(frozen=True, slots=True)
class Const(Expr):
    """Constant value: string, number, boolean, None."""

    value: str | int | float | bool | None


@dataclass(frozen=True, slots=True)
class Name(Expr):
    """Variable reference: {{ user }}"""

    name: str
    ctx: Literal["load", "store", "del"] = "load"


@dataclass(frozen=True, slots=True)
class Tuple(Expr):
    """Tuple expression: (a, b, c)"""

    items: Sequence[Expr]
    ctx: Literal["load", "store"] = "load"


@dataclass(frozen=True, slots=True)
class List(Expr):
    """List expression: [a, b, c]"""

    items: Sequence[Expr]


@dataclass(frozen=True, slots=True)
class Dict(Expr):
    """Dict expression: {a: b, c: d}"""

    keys: Sequence[Expr]
    values: Sequence[Expr]


@dataclass(frozen=True, slots=True)
class Getattr(Expr):
    """Attribute access: obj.attr"""

    obj: Expr
    attr: str


@dataclass(frozen=True, slots=True)
class OptionalGetattr(Expr):
    """Optional attribute access: obj?.attr

    Returns None if obj is None/undefined, otherwise obj.attr.
    Part of RFC: kida-modern-syntax-features.
    """

    obj: Expr
    attr: str


@dataclass(frozen=True, slots=True)
class Getitem(Expr):
    """Subscript access: obj[key]"""

    obj: Expr
    key: Expr


@dataclass(frozen=True, slots=True)
class OptionalGetitem(Expr):
    """Optional subscript access: obj?[key]

    Returns None if obj is None/undefined, otherwise obj[key].
    Part of RFC: kida-modern-syntax-features.
    """

    obj: Expr
    key: Expr


@dataclass(frozen=True, slots=True)
class Slice(Expr):
    """Slice expression: [start:stop:step]"""

    start: Expr | None
    stop: Expr | None
    step: Expr | None


@dataclass(frozen=True, slots=True)
class FuncCall(Expr):
    """Function call: func(args, **kwargs)"""

    func: Expr
    args: Sequence[Expr] = ()
    kwargs: dict[str, Expr] = field(default_factory=dict)
    dyn_args: Expr | None = None  # *args
    dyn_kwargs: Expr | None = None  # **kwargs


@dataclass(frozen=True, slots=True)
class Filter(Expr):
    """Filter application: expr | filter(args)"""

    value: Expr
    name: str
    args: Sequence[Expr] = ()
    kwargs: dict[str, Expr] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class Pipeline(Expr):
    """Pipeline operator: expr |> filter1 |> filter2

    Kida-native syntax for readable filter chains.
    More readable than deeply nested Jinja filters.

    Example:
        {{ items |> where(published=true) |> sort_by("date") |> take(5) }}

    vs Jinja:
        {{ items | selectattr("published") | sort(attribute="date") | first }}
    """

    value: Expr
    steps: Sequence[tuple[str, Sequence[Expr], dict[str, Expr]]]  # (name, args, kwargs)


@dataclass(frozen=True, slots=True)
class Test(Expr):
    """Test application: expr is test(args) or expr is not test(args)"""

    value: Expr
    name: str
    args: Sequence[Expr] = ()
    kwargs: dict[str, Expr] = field(default_factory=dict)
    negated: bool = False  # True for "is not"


# =============================================================================
# Operators
# =============================================================================


@dataclass(frozen=True, slots=True)
class BinOp(Expr):
    """Binary operation: left op right"""

    op: str  # '+', '-', '*', '/', '//', '%', '**', '~'
    left: Expr
    right: Expr


@dataclass(frozen=True, slots=True)
class UnaryOp(Expr):
    """Unary operation: op operand"""

    op: str  # '-', '+', 'not'
    operand: Expr


@dataclass(frozen=True, slots=True)
class Compare(Expr):
    """Comparison: left op1 right1 op2 right2 ...

    Supports chained comparisons like: 1 < x < 10
    """

    left: Expr
    ops: Sequence[str]  # '<', '<=', '>', '>=', '==', '!=', 'in', 'not in', 'is', 'is not'
    comparators: Sequence[Expr]


@dataclass(frozen=True, slots=True)
class BoolOp(Expr):
    """Boolean operation: expr1 and/or expr2"""

    op: Literal["and", "or"]
    values: Sequence[Expr]


@dataclass(frozen=True, slots=True)
class CondExpr(Expr):
    """Conditional expression: a if cond else b"""

    test: Expr
    if_true: Expr
    if_false: Expr


@dataclass(frozen=True, slots=True)
class NullCoalesce(Expr):
    """Null coalescing: a ?? b

    Returns b if a is None/undefined, otherwise a.
    Unlike 'or', doesn't treat falsy values (0, '', False, []) as missing.
    Part of RFC: kida-modern-syntax-features.
    """

    left: Expr
    right: Expr


@dataclass(frozen=True, slots=True)
class Range(Expr):
    """Range literal: start..end or start...end

    Part of RFC: kida-modern-syntax-features.

    Attributes:
        start: Start value (inclusive)
        end: End value (inclusive if inclusive=True)
        inclusive: True for .., False for ...
        step: Optional step value (from 'by' keyword)
    """

    start: Expr
    end: Expr
    inclusive: bool = True
    step: Expr | None = None


# =============================================================================
# Async Expressions (Kida native)
# =============================================================================


@dataclass(frozen=True, slots=True)
class Await(Expr):
    """Await expression: await expr

    Native async support without auto_await() wrappers.
    """

    value: Expr


# =============================================================================
# Special
# =============================================================================


@dataclass(frozen=True, slots=True)
class Concat(Expr):
    """String concatenation: a ~ b ~ c

    Multiple ~ operators are collapsed into a single Concat node
    for efficient string building.
    """

    nodes: Sequence[Expr]


@dataclass(frozen=True, slots=True)
class MarkSafe(Expr):
    """Mark expression as safe (no escaping): {{ expr | safe }}"""

    value: Expr


@dataclass(frozen=True, slots=True)
class LoopVar(Expr):
    """Loop variable access: {{ loop.index }}

    Provides access to loop iteration state.
    """

    attr: str  # 'index', 'index0', 'first', 'last', 'length', etc.


@dataclass(frozen=True, slots=True)
class InlinedFilter(Expr):
    """Inlined filter as direct method call (optimization).

    Generated by FilterInliner for common pure filters like upper, lower, strip.
    The compiler generates `str(value).method()` instead of filter dispatch.

    Example:
        `{{ name | upper }}` compiles to `str(ctx["name"]).upper()`
        instead of `_filters['upper'](ctx["name"])`
    """

    value: Expr  # The expression being filtered
    method: str  # The str method to call (e.g., "upper", "lower")
    args: Sequence[Expr] = ()  # Optional method arguments


# =============================================================================
# Type Aliases
# =============================================================================

AnyExpr = (
    Const
    | Name
    | Tuple
    | List
    | Dict
    | Getattr
    | OptionalGetattr
    | Getitem
    | OptionalGetitem
    | Slice
    | FuncCall
    | Filter
    | Pipeline
    | Test
    | BinOp
    | UnaryOp
    | Compare
    | BoolOp
    | CondExpr
    | NullCoalesce
    | Range
    | Await
    | Concat
    | MarkSafe
    | LoopVar
    | InlinedFilter
)
