"""Safe, sandboxed ``{{ }}`` templating over prior node outputs (build plan 8.1).

- ``render_value`` interpolates ``{{ path }}`` references into strings, dicts, and
  lists. A value that is exactly one reference keeps its original type.
- ``evaluate_condition`` evaluates a boolean expression like
  ``{{ a1.output.priority }} == 'high'``. References are bound to opaque names and
  the expression is evaluated by a hand-written AST walker that only permits
  literals, comparisons, boolean/`not` operators, and membership — never calls,
  attribute access, subscripts, or arbitrary names. No code execution.
"""

from __future__ import annotations

import ast
import re
from typing import Any

_REF = re.compile(r"\{\{\s*(.*?)\s*\}\}")


class ExpressionError(ValueError):
    """Raised when a condition expression uses a disallowed construct."""


def resolve_path(path: str, context: dict) -> Any:
    current: Any = context
    for part in path.strip().split("."):
        if isinstance(current, dict):
            current = current.get(part)
        elif isinstance(current, list):
            try:
                current = current[int(part)]
            except (ValueError, IndexError):
                return None
        else:
            return None
        if current is None:
            return None
    return current


def render_value(value: Any, context: dict) -> Any:
    if isinstance(value, str):
        return _render_str(value, context)
    if isinstance(value, dict):
        return {k: render_value(v, context) for k, v in value.items()}
    if isinstance(value, list):
        return [render_value(v, context) for v in value]
    return value


def _render_str(text: str, context: dict) -> Any:
    whole = _REF.fullmatch(text.strip())
    if whole:  # a lone reference preserves the resolved value's type
        return resolve_path(whole.group(1), context)

    def repl(match: re.Match) -> str:
        resolved = resolve_path(match.group(1), context)
        return "" if resolved is None else str(resolved)

    return _REF.sub(repl, text)


_COMPARATORS = {
    ast.Eq: lambda a, b: a == b,
    ast.NotEq: lambda a, b: a != b,
    ast.Lt: lambda a, b: a < b,
    ast.LtE: lambda a, b: a <= b,
    ast.Gt: lambda a, b: a > b,
    ast.GtE: lambda a, b: a >= b,
    ast.In: lambda a, b: a in b,
    ast.NotIn: lambda a, b: a not in b,
}


def _bind_refs(expr: str, context: dict) -> tuple[str, dict]:
    values: dict[str, Any] = {}

    def repl(match: re.Match) -> str:
        name = f"__v{len(values)}"
        values[name] = resolve_path(match.group(1), context)
        return name

    return _REF.sub(repl, expr), values


def _eval(node: ast.AST, values: dict) -> Any:
    if isinstance(node, ast.Constant):
        return node.value
    if isinstance(node, ast.Name):
        if node.id in values:
            return values[node.id]
        raise ExpressionError(f"unknown name in expression: {node.id}")
    if isinstance(node, ast.BoolOp):
        results = [_eval(v, values) for v in node.values]
        return all(results) if isinstance(node.op, ast.And) else any(results)
    if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
        return not _eval(node.operand, values)
    if isinstance(node, ast.Compare):
        left = _eval(node.left, values)
        for op, comparator in zip(node.ops, node.comparators, strict=True):
            fn = _COMPARATORS.get(type(op))
            if fn is None:
                raise ExpressionError(f"disallowed comparison: {type(op).__name__}")
            right = _eval(comparator, values)
            if not fn(left, right):
                return False
            left = right
        return True
    if isinstance(node, ast.List | ast.Tuple | ast.Set):
        return [_eval(e, values) for e in node.elts]
    raise ExpressionError(f"disallowed expression element: {type(node).__name__}")


def evaluate_condition(expr: str, context: dict) -> bool:
    bound, values = _bind_refs(expr, context)
    try:
        tree = ast.parse(bound.strip(), mode="eval")
    except SyntaxError as exc:
        raise ExpressionError(f"invalid expression: {exc}") from exc
    return bool(_eval(tree.body, values))
