"""Policy linting — catch common policy mistakes before production.

Detects issues like:
- Overlapping policies with contradictory effects
- Roles referenced in policies but never defined
- Unreachable policies shadowed by higher-priority rules
- Duplicate policy names (caught at load time, but lint catches YAML issues)
- Policies with no permissions and no conditions (matches everything)

Usage:
    from pyauthz.core.lint import lint_policies

    issues = lint_policies(pyauthz)
    for issue in issues:
        print(issue)
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import StrEnum

from pyauthz.core.engine import PyAuthz
from pyauthz.models.permission import Permission
from pyauthz.models.policy import Policy
from pyauthz.models.role import Role


class Severity(StrEnum):
    """Lint issue severity levels."""

    ERROR = "error"
    WARNING = "warning"
    INFO = "info"


@dataclass
class LintIssue:
    """A single lint issue found in the policy configuration.

    Attributes:
        severity: How serious the issue is.
        code: A short code for the issue type (e.g., "L001").
        message: Human-readable description.
        policy_name: The policy that triggered the issue (if applicable).
        role_name: The role that triggered the issue (if applicable).
    """

    severity: Severity
    code: str
    message: str
    policy_name: str | None = None
    role_name: str | None = None

    def __str__(self) -> str:
        context = ""
        if self.policy_name:
            context = f" [policy: {self.policy_name}]"
        elif self.role_name:
            context = f" [role: {self.role_name}]"
        return f"[{self.severity.value}] {self.code}: {self.message}{context}"


def lint_policies(pyauthz: PyAuthz) -> list[LintIssue]:
    """Lint the current policy configuration for common issues.

    Args:
        pyauthz: The PyAuthz engine to lint.

    Returns:
        List of LintIssue objects, sorted by severity (errors first).
    """
    issues: list[LintIssue] = []
    roles = pyauthz._storage.get_roles()
    policies = pyauthz._storage.get_policies()

    issues.extend(_check_undefined_roles(policies, roles))
    issues.extend(_check_overly_broad_policies(policies))
    issues.extend(_check_contradictory_policies(policies))
    issues.extend(_check_disabled_policies(policies))
    issues.extend(_check_orphan_roles(policies, roles))
    issues.extend(_check_undefined_parents(roles))

    # Sort: errors first, then warnings, then info
    severity_order = {Severity.ERROR: 0, Severity.WARNING: 1, Severity.INFO: 2}
    issues.sort(key=lambda i: severity_order.get(i.severity, 3))

    return issues


def _check_undefined_roles(policies: list[Policy], roles: dict[str, Role]) -> list[LintIssue]:
    """Check for roles referenced in policies but not defined."""
    issues: list[LintIssue] = []
    for policy in policies:
        for role_name in policy.roles:
            if role_name not in roles:
                issues.append(
                    LintIssue(
                        severity=Severity.ERROR,
                        code="L001",
                        message=f"Role '{role_name}' is referenced but not defined.",
                        policy_name=policy.name,
                    )
                )
    return issues


def _check_overly_broad_policies(policies: list[Policy]) -> list[LintIssue]:
    """Check for policies that are overly broad (no roles, no conditions, wildcard perms)."""
    issues: list[LintIssue] = []
    for policy in policies:
        if not policy.roles and not policy.conditions:
            # Policy applies to everyone with no conditions
            has_wildcard = any(p == "*:*" for p in (policy.permissions or []))
            if has_wildcard or not policy.permissions:
                issues.append(
                    LintIssue(
                        severity=Severity.WARNING,
                        code="L002",
                        message=(
                            "Policy has no role restriction and no conditions, "
                            "and matches all permissions. This applies to every request."
                        ),
                        policy_name=policy.name,
                    )
                )
    return issues


def _check_contradictory_policies(policies: list[Policy]) -> list[LintIssue]:
    """Check for policies with overlapping permissions but contradictory effects."""
    issues: list[LintIssue] = []

    for i, p1 in enumerate(policies):
        for p2 in policies[i + 1 :]:
            if p1.effect == p2.effect:
                continue
            if not _roles_overlap(p1.roles, p2.roles):
                continue
            if not _permissions_overlap(p1.permissions, p2.permissions):
                continue
            issues.append(
                LintIssue(
                    severity=Severity.WARNING,
                    code="L003",
                    message=(
                        f"Policy '{p1.name}' ({p1.effect.value}) and "
                        f"'{p2.name}' ({p2.effect.value}) have overlapping "
                        f"roles and permissions with contradictory effects."
                    ),
                    policy_name=p1.name,
                )
            )

    return issues


def _check_disabled_policies(policies: list[Policy]) -> list[LintIssue]:
    """Check for disabled policies."""
    issues: list[LintIssue] = []
    for policy in policies:
        if not policy.enabled:
            issues.append(
                LintIssue(
                    severity=Severity.INFO,
                    code="L004",
                    message="Policy is disabled.",
                    policy_name=policy.name,
                )
            )
    return issues


def _check_orphan_roles(policies: list[Policy], roles: dict[str, Role]) -> list[LintIssue]:
    """Check for roles that are defined but never referenced in any policy."""
    if not policies:
        return []

    referenced_roles: set[str] = set()
    for policy in policies:
        referenced_roles.update(policy.roles)

    # Also count roles referenced as parents
    for role in roles.values():
        referenced_roles.update(role.parents)

    issues: list[LintIssue] = []
    for role_name in roles:
        if role_name not in referenced_roles:
            issues.append(
                LintIssue(
                    severity=Severity.INFO,
                    code="L005",
                    message=(
                        f"Role '{role_name}' is defined but not referenced "
                        f"in any policy or as a parent of another role."
                    ),
                    role_name=role_name,
                )
            )

    return issues


def _check_undefined_parents(roles: dict[str, Role]) -> list[LintIssue]:
    """Check for roles that reference undefined parent roles."""
    issues: list[LintIssue] = []
    for role in roles.values():
        for parent in role.parents:
            if parent not in roles:
                issues.append(
                    LintIssue(
                        severity=Severity.WARNING,
                        code="L006",
                        message=f"Parent role '{parent}' is not defined.",
                        role_name=role.name,
                    )
                )
    return issues


def _roles_overlap(roles1: list[str], roles2: list[str]) -> bool:
    """Check if two role lists overlap (or either is empty = applies to all)."""
    if not roles1 or not roles2:
        return True
    return bool(set(roles1) & set(roles2))


def _permissions_overlap(perms1: list[str] | None, perms2: list[str] | None) -> bool:
    """Check if two permission lists could overlap."""
    if not perms1 or not perms2:
        return True

    for p1_str in perms1:
        for p2_str in perms2:
            try:
                p1 = Permission.from_string(p1_str)
                p2 = Permission.from_string(p2_str)
            except ValueError:
                continue
            if p1.matches(p2.resource_type, p2.action) or p2.matches(p1.resource_type, p1.action):
                return True

    return False
