"""Policy diffing — compare two policy sets and report impact.

Analyzes what changed between two sets of policies/roles and identifies
scenarios where the authorization outcome changes (allow→deny or deny→allow).

Usage:
    from pyauthz.core.diff import diff_policies

    changes = diff_policies(old_pyauthz, new_pyauthz, scenarios)
    for change in changes:
        print(change)
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from pyauthz.core.engine import PyAuthz
from pyauthz.models.resource import Resource
from pyauthz.models.subject import Subject


@dataclass
class PolicyChange:
    """A single changed authorization outcome between two policy sets.

    Attributes:
        subject: The subject in the scenario.
        action: The action checked.
        resource: The resource checked.
        old_allowed: Whether the old policies allowed access.
        new_allowed: Whether the new policies allow access.
        old_reasoning: Reasoning from the old evaluation.
        new_reasoning: Reasoning from the new evaluation.
    """

    subject: Subject
    action: str
    resource: Resource
    old_allowed: bool
    new_allowed: bool
    old_reasoning: list[str] = field(default_factory=list)
    new_reasoning: list[str] = field(default_factory=list)

    @property
    def change_type(self) -> str:
        """Describe the change: 'allow→deny', 'deny→allow', or 'unchanged'."""
        if self.old_allowed and not self.new_allowed:
            return "allow→deny"
        if not self.old_allowed and self.new_allowed:
            return "deny→allow"
        return "unchanged"

    def __str__(self) -> str:
        old = "ALLOW" if self.old_allowed else "DENY"
        new = "ALLOW" if self.new_allowed else "DENY"
        return (
            f"{self.subject.id} -> {self.action} on "
            f"{self.resource.type}/{self.resource.id}: {old} → {new}"
        )


@dataclass
class DiffReport:
    """Summary of all changes between two policy sets.

    Attributes:
        changes: List of PolicyChange objects for scenarios that changed.
        unchanged_count: Number of scenarios that didn't change.
        total_scenarios: Total number of scenarios evaluated.
    """

    changes: list[PolicyChange] = field(default_factory=list)
    unchanged_count: int = 0
    total_scenarios: int = 0

    @property
    def has_changes(self) -> bool:
        """Whether any scenario changed."""
        return len(self.changes) > 0

    def summary(self) -> str:
        """Human-readable summary."""
        if not self.has_changes:
            return f"No changes detected across {self.total_scenarios} scenario(s)."
        allow_to_deny = sum(1 for c in self.changes if c.change_type == "allow→deny")
        deny_to_allow = sum(1 for c in self.changes if c.change_type == "deny→allow")
        parts = [
            f"{len(self.changes)} change(s) detected across {self.total_scenarios} scenario(s)"
        ]
        if allow_to_deny:
            parts.append(f"  {allow_to_deny} allow→deny (access revoked)")
        if deny_to_allow:
            parts.append(f"  {deny_to_allow} deny→allow (access granted)")
        return "\n".join(parts)


def diff_policies(
    old: PyAuthz,
    new: PyAuthz,
    scenarios: list[tuple[Subject, str, Resource]],
    *,
    env: dict[str, Any] | None = None,
    include_unchanged: bool = False,
) -> DiffReport:
    """Compare authorization outcomes between two PyAuthz engines.

    Evaluates each scenario against both engines and reports changes.

    Args:
        old: The original PyAuthz engine.
        new: The modified PyAuthz engine.
        scenarios: List of (Subject, action, Resource) tuples to compare.
        env: Environment context for evaluations.
        include_unchanged: If True, include unchanged scenarios in the report.

    Returns:
        DiffReport with changes and summary statistics.
    """
    merged_env = env or {}
    report = DiffReport(total_scenarios=len(scenarios))

    for subject, action, resource in scenarios:
        old_decision = old._evaluate(subject, action, resource, merged_env)
        new_decision = new._evaluate(subject, action, resource, merged_env)

        change = PolicyChange(
            subject=subject,
            action=action,
            resource=resource,
            old_allowed=old_decision.allowed,
            new_allowed=new_decision.allowed,
            old_reasoning=old_decision.reasoning,
            new_reasoning=new_decision.reasoning,
        )

        if change.change_type != "unchanged" or include_unchanged:
            report.changes.append(change)
        else:
            report.unchanged_count += 1

    return report
