"""Policy combination algorithms.

These determine how multiple matching policies are combined into a final decision.

Available combiners:
- deny-override (default): if ANY applicable policy says DENY, the result is DENY.
- allow-override: if ANY applicable policy says ALLOW, the result is ALLOW.
- priority-first: the highest-priority applicable policy wins.
- first-match: the first applicable policy in evaluation order wins.
"""

from collections.abc import Callable

from pyauthz.models.action import Effect
from pyauthz.models.policy import Policy

CombinerFunc = Callable[[list[Policy]], tuple[Effect, list[str], list[str]]]


def deny_override(policies: list[Policy]) -> tuple[Effect, list[str], list[str]]:
    """Deny-override combiner: any DENY wins over all ALLOWs.

    Args:
        policies: The list of applicable (matched) policies.

    Returns:
        A tuple of (final_effect, matched_policy_names, reasoning).
    """
    if not policies:
        return Effect.DENY, [], ["no matching policy"]

    matched_names: list[str] = []
    reasoning: list[str] = []
    has_deny = False
    has_allow = False

    for policy in policies:
        matched_names.append(policy.name)
        if policy.effect == Effect.DENY:
            has_deny = True
            reasoning.append(f"Policy '{policy.name}' explicitly denies access")
        else:
            has_allow = True
            reasoning.append(f"Policy '{policy.name}' allows access")

    if has_deny:
        return Effect.DENY, matched_names, reasoning
    if has_allow:
        return Effect.ALLOW, matched_names, reasoning

    return Effect.DENY, matched_names, ["no matching policy"]


def allow_override(policies: list[Policy]) -> tuple[Effect, list[str], list[str]]:
    """Allow-override combiner: any ALLOW wins over all DENYs.

    The inverse of deny-override. If any applicable policy says ALLOW,
    the result is ALLOW regardless of other DENY policies.

    Args:
        policies: The list of applicable (matched) policies.

    Returns:
        A tuple of (final_effect, matched_policy_names, reasoning).
    """
    if not policies:
        return Effect.DENY, [], ["no matching policy"]

    matched_names: list[str] = []
    reasoning: list[str] = []
    has_deny = False
    has_allow = False

    for policy in policies:
        matched_names.append(policy.name)
        if policy.effect == Effect.ALLOW:
            has_allow = True
            reasoning.append(f"Policy '{policy.name}' allows access")
        else:
            has_deny = True
            reasoning.append(f"Policy '{policy.name}' explicitly denies access")

    if has_allow:
        return Effect.ALLOW, matched_names, reasoning
    if has_deny:
        return Effect.DENY, matched_names, reasoning

    return Effect.DENY, matched_names, ["no matching policy"]


def priority_first(policies: list[Policy]) -> tuple[Effect, list[str], list[str]]:
    """Priority-first combiner: the highest-priority applicable policy wins.

    Sorts by priority (descending), then takes the first policy.
    Ties are broken by evaluation order (first encountered wins).

    Args:
        policies: The list of applicable (matched) policies.

    Returns:
        A tuple of (final_effect, matched_policy_names, reasoning).
    """
    if not policies:
        return Effect.DENY, [], ["no matching policy"]

    # Sort by priority descending; stable sort preserves original order for ties
    sorted_policies = sorted(policies, key=lambda p: p.priority, reverse=True)
    winner = sorted_policies[0]
    effect_str = "allows" if winner.effect == Effect.ALLOW else "denies"
    reasoning = [
        f"Policy '{winner.name}' (priority {winner.priority}, highest) {effect_str} access"
    ]
    matched_names = [p.name for p in sorted_policies]
    return winner.effect, matched_names, reasoning


def first_match(policies: list[Policy]) -> tuple[Effect, list[str], list[str]]:
    """First-match combiner: the first applicable policy wins.

    Args:
        policies: The list of applicable (matched) policies, in evaluation order.

    Returns:
        A tuple of (final_effect, matched_policy_names, reasoning).
    """
    if not policies:
        return Effect.DENY, [], ["no matching policy"]

    first = policies[0]
    effect_str = "allows" if first.effect == Effect.ALLOW else "denies"
    reasoning = [f"Policy '{first.name}' (first match) {effect_str} access"]
    return first.effect, [first.name], reasoning


COMBINERS: dict[str, CombinerFunc] = {
    "deny-override": deny_override,
    "allow-override": allow_override,
    "priority-first": priority_first,
    "first-match": first_match,
}
