"""Safe boolean-expression evaluator for transition guards.

Mirrors the shape of the workflow DSL described in `registry.ConditionSpec`.
No `eval`/import/getattr indirection — only declarative comparisons against
the runtime context (subject attributes, instance data, actor permissions).
"""

from __future__ import annotations

import operator
from collections.abc import Callable, Mapping
from typing import Any

from simorgh.apps.iam.engine import is_allowed
from simorgh.apps.workflow.registry import ActionContext, ConditionSpec, WorkflowError
from simorgh.core.context import current_request_context

_MAX_DEPTH = 8

_COMPARATORS: dict[str, Callable[[Any, Any], bool]] = {
    "eq": operator.eq,
    "ne": operator.ne,
    "lt": operator.lt,
    "lte": operator.le,
    "gt": operator.gt,
    "gte": operator.ge,
    "in": lambda a, b: a in b,
    "nin": lambda a, b: a not in b,
    "contains": lambda a, b: b in a,
    "is_null": lambda a, _b: a is None,
}


def evaluate_conditions(
    conditions: tuple[ConditionSpec, ...],
    ctx: ActionContext,
) -> bool:
    """Return True iff every condition evaluates truthy."""

    return all(_eval_node(c.expression, ctx, 0) for c in conditions)


def _eval_node(node: Any, ctx: ActionContext, depth: int) -> bool:
    if depth > _MAX_DEPTH:
        raise WorkflowError("condition expression exceeds max depth")
    if not isinstance(node, Mapping):
        raise WorkflowError(f"condition node must be a mapping, got {type(node).__name__}")

    if "all" in node:
        children = node["all"]
        return all(_eval_node(c, ctx, depth + 1) for c in children)
    if "any" in node:
        children = node["any"]
        return any(_eval_node(c, ctx, depth + 1) for c in children)
    if "not" in node:
        return not _eval_node(node["not"], ctx, depth + 1)

    if "permission" in node:
        req = current_request_context()
        return is_allowed(req, str(node["permission"]))

    if "field" in node:
        op_name = node.get("op", "eq")
        comparator = _COMPARATORS.get(op_name)
        if comparator is None:
            raise WorkflowError(f"unknown condition op {op_name!r}")
        actual = _resolve_field(node["field"], ctx)
        if op_name == "is_null":
            return comparator(actual, None)
        return comparator(actual, node.get("value"))

    raise WorkflowError(f"unrecognized condition node keys: {sorted(node.keys())}")


def _resolve_field(path: str, ctx: ActionContext) -> Any:
    """Resolve a dotted path against the action context.

    Supported roots:
      * ``subject.*``  → attributes/keys on the workflow instance's subject
      * ``instance.*`` → ``WorkflowInstance.data`` dict keys (or top-level attrs)
      * ``payload.*``  → keys on the dispatched payload (events / manual fires)
      * ``actor.*``    → attributes on ``ctx.actor``
    """

    if not isinstance(path, str) or not path:
        raise WorkflowError("condition field must be a non-empty dotted string")

    parts = path.split(".")
    root, rest = parts[0], parts[1:]

    if root == "subject":
        current: Any = getattr(ctx.instance, "subject", None)
    elif root == "instance":
        current = ctx.instance
        if rest and isinstance(getattr(current, "data", None), dict) and rest[0] in current.data:
            current = current.data[rest[0]]
            rest = rest[1:]
    elif root == "payload":
        current = ctx.payload
    elif root == "actor":
        current = ctx.actor
    else:
        raise WorkflowError(f"unknown condition field root {root!r}")

    for part in rest:
        if current is None:
            return None
        if isinstance(current, Mapping):
            current = current.get(part)
        else:
            current = getattr(current, part, None)
    return current
