from typing import Any

from .policy import evaluate as evaluate_policy


def _decide_single(obj: dict[str, Any], env: dict[str, Any]) -> dict[str, Any]:
    """Decide for a single policy or nested policy set."""
    if "policies" in obj:
        return decide(obj, env)
    return evaluate_policy(obj, env)


def _is_applicable(result: dict[str, Any]) -> bool:
    """A policy is applicable only if a concrete rule matched (has rule_id)."""
    rid = result.get("last_rule_id") or result.get("rule_id")
    return isinstance(rid, str) and rid != ""


def _merge_traces(
    existing: list[dict[str, Any]] | None,
    result: dict[str, Any],
) -> list[dict[str, Any]] | None:
    """Append trace entries from *result* into *existing*, return updated list.

    Returns ``None`` when trace collection is inactive (existing is None).
    Entries from *result* that are already plain dicts are appended as-is.
    """
    if existing is None:
        return None
    child_trace = result.get("trace")
    if isinstance(child_trace, list):
        existing.extend(child_trace)
    return existing


def decide(policyset: dict[str, Any], env: dict[str, Any]) -> dict[str, Any]:
    """Evaluate a policy set with combining algorithm over its child policies."""
    algo = (policyset.get("algorithm") or "deny-overrides").lower()
    policies = policyset.get("policies") or []

    # Trace: only allocate when explain mode is active.
    collect_trace: bool = bool(env.get("__explain__"))
    trace: list[dict[str, Any]] | None = [] if collect_trace else None

    if not isinstance(policies, list):
        return {
            "decision": "deny",
            "reason": "no_match",
            "rule_id": None,
            "last_rule_id": None,
            "policy_id": None,
            "obligations": [],
            "trace": trace,
        }

    any_permit: bool = False
    any_deny: bool = False

    first_applicable_result: dict[str, Any] | None = None
    first_applicable_pid: str | None = None

    permit_result: dict[str, Any] | None = None
    permit_pid: str | None = None

    deny_result: dict[str, Any] | None = None
    deny_pid: str | None = None

    last_rule_id: str | None = None

    for pol in policies:
        pid = pol.get("id")
        res = _decide_single(pol, env)

        # Accumulate trace from this child policy regardless of applicability.
        trace = _merge_traces(trace, res)

        rid = res.get("last_rule_id") or res.get("rule_id")
        if isinstance(rid, str) and rid:
            last_rule_id = rid

        if not _is_applicable(res):
            continue

        decision = str(res.get("decision") or "")
        if algo == "first-applicable":
            first_applicable_result = res
            first_applicable_pid = pid
            break

        if decision == "deny":
            any_deny = True
            if deny_result is None:
                deny_result = res
                deny_pid = pid
            if algo == "deny-overrides":
                break
        elif decision == "permit":
            any_permit = True
            if permit_result is None:
                permit_result = res
                permit_pid = pid
            if algo == "permit-overrides":
                break
        else:
            continue

    if algo == "first-applicable":
        if first_applicable_result is not None:
            out = dict(first_applicable_result)
            out["policy_id"] = first_applicable_pid
            out["trace"] = trace
            return out
        return {
            "decision": "deny",
            "reason": "no_match",
            "rule_id": None,
            "last_rule_id": last_rule_id,
            "policy_id": None,
            "obligations": [],
            "trace": trace,
        }

    if algo == "deny-overrides":
        if any_deny and deny_result is not None:
            return {
                "decision": "deny",
                "reason": "explicit_deny",
                "rule_id": deny_result.get("last_rule_id") or deny_result.get("rule_id"),
                "last_rule_id": deny_result.get("last_rule_id") or deny_result.get("rule_id"),
                "policy_id": deny_pid,
                "obligations": list(deny_result.get("obligations") or []),
                "trace": trace,
            }
        if any_permit and permit_result is not None:
            out = dict(permit_result)
            out["policy_id"] = permit_pid
            out["reason"] = out.get("reason") or "matched"
            out["trace"] = trace
            return out
        return {
            "decision": "deny",
            "reason": "no_match",
            "rule_id": None,
            "last_rule_id": last_rule_id,
            "policy_id": None,
            "obligations": [],
            "trace": trace,
        }

    # permit-overrides
    if any_permit and permit_result is not None:
        out = dict(permit_result)
        out["policy_id"] = permit_pid
        out["reason"] = out.get("reason") or "matched"
        out["trace"] = trace
        return out
    if any_deny and deny_result is not None:
        return {
            "decision": "deny",
            "reason": "explicit_deny",
            "rule_id": deny_result.get("last_rule_id") or deny_result.get("rule_id"),
            "last_rule_id": deny_result.get("last_rule_id") or deny_result.get("rule_id"),
            "policy_id": deny_pid,
            "obligations": list(deny_result.get("obligations") or []),
            "trace": trace,
        }

    return {
        "decision": "deny",
        "reason": "no_match",
        "rule_id": None,
        "last_rule_id": last_rule_id,
        "policy_id": None,
        "obligations": [],
        "trace": trace,
    }


__all__ = ["decide"]
