"""Flow-control nodes — ConditionalNode.

- ConditionalNode: routes to different targets based on a condition expression
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Literal

from core.pathways.expressions import SafeExpressionError, safe_eval
from core.pathways.models.io import NodeInputs, NodeOutputs
from core.pathways.nodes.base import NodeBase
from core.pathways.nodes.models import ConditionalOutput

if TYPE_CHECKING:
    from core.pathways.context import Context

logger = logging.getLogger(__name__)


class ConditionalNode(NodeBase):
    """Conditional branching node — routes to different targets based on a condition.

    Evaluates the condition expression and returns which branch to take.
    The pathway VM uses the result to determine the next node.

    Supports two condition styles:

    **Direct expression (preferred)**::

        condition: 'classify.category == "emergency"'

    The expression is evaluated directly via ``safe_eval`` with upstream
    node outputs available as dict variables.  Dot-access sugar means
    ``classify.category`` resolves to ``classify["category"]``.

    **Template expression (legacy)**::

        condition: '{{classify.category}} == "emergency"'

    Jinja2 templates are resolved first, then the result is evaluated.
    This is fragile because template resolution produces unquoted strings,
    so prefer direct expressions for new pathways.

    Attributes
    ----------
    condition : str
        Expression to evaluate.
    true_node : str
        Node ID to go to if condition is true.
    false_node : str
        Node ID to go to if condition is false.
    """

    type: Literal["conditional"] = "conditional"
    condition: str
    true_node: str
    false_node: str

    async def compute(self, inputs: NodeInputs, _ctx: Context) -> NodeOutputs:
        """Evaluate condition and return branch decision.

        Uses a three-tier evaluation strategy:

        1. **Direct eval** — evaluate the raw condition as a ``safe_eval``
           expression with the full input data as the environment.  This
           handles ``classify.category == "emergency"`` cleanly.
        2. **Template + eval** — if direct eval fails (e.g. the condition
           contains ``{{ }}`` Jinja2 markers), resolve templates first, then
           evaluate the result.
        3. **Truthy fallback** — if both fail, treat the template-resolved
           string as a truthy/falsy value.  This ensures the node never
           crashes the pipeline even with malformed expressions.
        """
        template_data = inputs.to_dict()
        has_templates = "{{" in self.condition or "{%" in self.condition

        branch: bool

        # --- Tier 1: Direct expression eval (preferred) ---
        if not has_templates:
            try:
                branch = bool(safe_eval(self.condition, env=template_data))
                return self._build_output(branch)
            except SafeExpressionError:
                logger.debug(
                    "[ConditionalNode %s] Direct eval failed for %r, trying template fallback",
                    self.id,
                    self.condition[:80],
                )

        # --- Tier 2: Template resolution + eval ---
        # Use expression-aware template rendering that auto-quotes strings
        # so {{node.urgency}} == "HIGH" becomes "HIGH" == "HIGH" (valid).
        formatted = self._format_template_for_expr(self.condition, template_data)
        try:
            branch = bool(safe_eval(formatted, env=template_data))
            return self._build_output(branch)
        except (SafeExpressionError, Exception):
            logger.warning(
                "[ConditionalNode %s] Expression eval failed for condition %r "
                "(resolved to %r). Falling back to truthy check. "
                "Prefer direct expressions without {{ }}, e.g.: "
                'classify.urgency == "HIGH" instead of '
                '{{classify.urgency}} == "HIGH"',
                self.id,
                self.condition[:80],
                formatted[:80],
            )

        # --- Tier 3: Truthy fallback ---
        branch = bool(
            formatted
            and formatted.strip() not in ("", "0", "false", "False", "none", "None", "null")
        )
        return self._build_output(branch)

    def _build_output(self, branch: bool) -> NodeOutputs:
        """Construct the branch decision output."""
        target = self.true_node if branch else self.false_node
        output = ConditionalOutput(
            branch="true" if branch else "false",
            next_node=target,
            selected_route=target,
            condition_result=branch,
        )
        return NodeOutputs(output=output.model_dump(), selected_route=target)


__all__ = [
    "ConditionalNode",
]
