"""Tiny declarative condition evaluator for ABAC policies.

Supported expression shape (recursive):
  {"all": [<expr>, ...]}                     # AND
  {"any": [<expr>, ...]}                     # OR
  {"not": <expr>}
  {"==": [<lhs>, <rhs>]} / "!=" / "<" / "<=" / ">" / ">=" / "in"
  {"attr": "actor.id"}    -- dotted-path lookup into the eval context
  {"const": <literal>}    -- a literal value (optional; bare literals also work)

Anything not a dict is treated as a literal. Unknown operators raise.

This is intentionally **small**. We do NOT eval Python code. If a future
phase needs richer rules (e.g. CEL), it can swap the evaluator behind the
same `evaluate(expr, ctx)` entry point.
"""

from __future__ import annotations

from typing import Any

_MISSING = object()


class PolicyEvalError(Exception):
    pass


def _lookup(path: str, ctx: dict[str, Any]) -> Any:
    cur: Any = ctx
    for part in path.split("."):
        cur = cur.get(part, _MISSING) if isinstance(cur, dict) else getattr(cur, part, _MISSING)
        if cur is _MISSING:
            return None
    return cur


def _resolve(node: Any, ctx: dict[str, Any]) -> Any:
    if isinstance(node, dict):
        if "attr" in node:
            return _lookup(node["attr"], ctx)
        if "const" in node:
            return node["const"]
    return node


def evaluate(expr: Any, ctx: dict[str, Any]) -> bool:
    if expr in (None, {}, True):
        return True
    if expr is False:
        return False
    if not isinstance(expr, dict):
        return bool(expr)

    if "all" in expr:
        return all(evaluate(e, ctx) for e in expr["all"])
    if "any" in expr:
        return any(evaluate(e, ctx) for e in expr["any"])
    if "not" in expr:
        return not evaluate(expr["not"], ctx)

    for op in ("==", "!=", "<", "<=", ">", ">=", "in"):
        if op in expr:
            lhs, rhs = expr[op]
            lhs_v, rhs_v = _resolve(lhs, ctx), _resolve(rhs, ctx)
            return _COMPARATORS[op](lhs_v, rhs_v)

    if "same_org_node" in expr:
        lhs, rhs = expr["same_org_node"]
        lhs_v, rhs_v = _resolve(lhs, ctx), _resolve(rhs, ctx)
        return lhs_v is not None and lhs_v == rhs_v

    if "subtree_contains" in expr:
        # {"subtree_contains": [<ancestor_id>, <descendant_id>]}
        ancestor, descendant = expr["subtree_contains"]
        ancestor_id = _resolve(ancestor, ctx)
        descendant_id = _resolve(descendant, ctx)
        if ancestor_id is None or descendant_id is None:
            return False
        from simorgh.apps.organizations.models import OrganizationNode

        try:
            node = OrganizationNode.objects.get(pk=descendant_id)
        except OrganizationNode.DoesNotExist:
            return False
        cur = node
        # Walk up the parent chain looking for the ancestor.
        for _ in range(64):  # depth guard
            if cur.pk == ancestor_id:
                return True
            if cur.parent_id is None:
                return False
            cur = cur.parent
        return False

    raise PolicyEvalError(f"unknown policy expression: {expr!r}")


_COMPARATORS = {
    "==": lambda a, b: a == b,
    "!=": lambda a, b: a != b,
    "<": lambda a, b: a < b,
    "<=": lambda a, b: a <= b,
    ">": lambda a, b: a > b,
    ">=": lambda a, b: a >= b,
    "in": lambda a, b: a in (b or ()),
}
