"""Policy analysis for detecting conflicts, duplicates, and shadows in SCPs."""

import fnmatch

from ..models.analysis import AnalysisReport, Issue, IssueType
from ..models.scp import SCPPolicy, SCPStatement
from .conditions import condition_is_subset, conditions_could_overlap


def actions_overlap(action1: str, action2: str) -> bool:
    """
    Determine if two action patterns overlap (could match the same action).

    Examples:
        actions_overlap("s3:*", "s3:GetObject") -> True (s3:* covers s3:GetObject)
        actions_overlap("s3:Get*", "s3:GetObject") -> True
        actions_overlap("s3:Get*", "s3:Put*") -> False (no overlap)
        actions_overlap("*", "anything:Here") -> True (* covers everything)
        actions_overlap("s3:GetObject", "s3:GetObject") -> True (exact match)

    Args:
        action1: First action pattern
        action2: Second action pattern

    Returns:
        True if the patterns could match the same concrete action
    """
    a1 = action1.lower()
    a2 = action2.lower()

    # Exact match
    if a1 == a2:
        return True

    # Universal wildcard matches everything
    if a1 == "*" or a2 == "*":
        return True

    # Check if one pattern matches the other
    if _pattern_matches(a1, a2) or _pattern_matches(a2, a1):
        return True

    # Check if patterns could match the same concrete action
    return _patterns_could_overlap(a1, a2)


def _pattern_matches(pattern: str, concrete: str) -> bool:
    """Check if a pattern matches a potentially concrete action."""
    if "*" not in pattern:
        return pattern == concrete
    return fnmatch.fnmatch(concrete, pattern)


def _patterns_could_overlap(p1: str, p2: str) -> bool:
    """
    Check if two wildcard patterns could match the same concrete action.

    This is a heuristic - we check if the non-wildcard parts are compatible.
    """
    if "*" not in p1 and "*" not in p2:
        return False

    # Extract service and action parts
    s1_service = p1.split(":")[0] if ":" in p1 else ""
    s2_service = p2.split(":")[0] if ":" in p2 else ""
    s1_action = p1.split(":")[1] if ":" in p1 else p1
    s2_action = p2.split(":")[1] if ":" in p2 else p2

    # If both have services, they must match (or one is *)
    if s1_service and s2_service:
        if s1_service != "*" and s2_service != "*" and s1_service != s2_service:
            return False

    # Check if the action parts could overlap
    s1_prefix = s1_action.replace("*", "")
    s2_prefix = s2_action.replace("*", "")

    # If one is empty (just "*"), it overlaps with anything
    if not s1_prefix or not s2_prefix:
        return True

    # Check if one prefix starts with the other
    if s1_prefix.startswith(s2_prefix) or s2_prefix.startswith(s1_prefix):
        return True

    return False


def action_is_subset(narrower: str, broader: str) -> bool:
    """
    Determine if one action pattern is a subset of another.

    Returns True if all actions matched by 'narrower' are also matched by 'broader'.

    Examples:
        action_is_subset("s3:GetObject", "s3:*") -> True
        action_is_subset("s3:Get*", "s3:*") -> True
        action_is_subset("s3:*", "s3:GetObject") -> False
        action_is_subset("s3:*", "*") -> True
        action_is_subset("*", "s3:*") -> False
    """
    n = narrower.lower()
    b = broader.lower()

    # Same pattern - trivially a subset
    if n == b:
        return True

    # Universal wildcard covers everything
    if b == "*":
        return True

    # If broader is not *, then * is not a subset of it
    if n == "*":
        return False

    # Service-level wildcard: s3:* covers s3:GetObject, s3:Get*, etc.
    if b.endswith(":*"):
        b_service = b.split(":")[0]
        if ":" in n:
            n_service = n.split(":")[0]
            return n_service == b_service
        return False

    # Prefix wildcard: s3:Get* covers s3:GetObject, s3:GetBucket, etc.
    if "*" in b and "*" not in n:
        return fnmatch.fnmatch(n, b)

    # Both have wildcards - check if narrower's pattern is contained in broader's
    if "*" in b and "*" in n:
        b_prefix = b.replace("*", "")
        n_prefix = n.replace("*", "")

        if len(b_prefix) <= len(n_prefix) and n_prefix.startswith(b_prefix):
            return True

    return False


def resources_overlap(res1: str, res2: str) -> bool:
    """
    Determine if two resource patterns overlap.

    Handles ARN patterns like arn:aws:s3:::bucket/* and wildcards.

    Args:
        res1: First resource pattern
        res2: Second resource pattern

    Returns:
        True if the patterns could match the same resource
    """
    r1 = res1.lower()
    r2 = res2.lower()

    # Exact match or universal wildcard
    if r1 == r2 or r1 == "*" or r2 == "*":
        return True

    # Check if one pattern matches the other using fnmatch
    if _pattern_matches(r1, r2) or _pattern_matches(r2, r1):
        return True

    # For ARNs, check component-wise
    if r1.startswith("arn:") and r2.startswith("arn:"):
        return _arns_overlap(r1, r2)

    # Check prefix overlap for wildcard patterns
    return _patterns_could_overlap(r1, r2)


def _arns_overlap(arn1: str, arn2: str) -> bool:
    """Check if two ARN patterns could match the same resource."""
    parts1 = arn1.split(":")
    parts2 = arn2.split(":")

    # ARNs have 6 parts: arn:partition:service:region:account:resource
    for i in range(min(len(parts1), len(parts2))):
        p1 = parts1[i]
        p2 = parts2[i]

        # Wildcard in either part means they could match
        if p1 == "*" or p2 == "*":
            continue

        # Check pattern matching
        if "*" in p1 or "*" in p2:
            if not (_pattern_matches(p1, p2) or _pattern_matches(p2, p1)):
                # Check if patterns could overlap
                if not _patterns_could_overlap(p1, p2):
                    return False
        elif p1 != p2:
            return False

    return True


def resource_is_subset(narrower: str, broader: str) -> bool:
    """
    Determine if one resource pattern is a subset of another.

    Returns True if all resources matched by 'narrower' are also matched by 'broader'.

    Args:
        narrower: The potentially narrower (more specific) pattern
        broader: The potentially broader (more general) pattern

    Returns:
        True if narrower is a subset of broader
    """
    n = narrower.lower()
    b = broader.lower()

    # Same pattern or universal wildcard in broader
    if n == b or b == "*":
        return True

    # If broader is not *, then * is not a subset of it
    if n == "*":
        return False

    # Check if broader pattern covers narrower
    if "*" in b:
        if "*" not in n:
            return fnmatch.fnmatch(n, b)
        # Both have wildcards
        b_prefix = b.replace("*", "")
        n_prefix = n.replace("*", "")
        return n_prefix.startswith(b_prefix) and len(b_prefix) <= len(n_prefix)

    return False


def find_overlapping_resources(resources1: list[str], resources2: list[str]) -> list[str]:
    """Find resources that overlap between two lists."""
    overlaps = []
    for r1 in resources1:
        for r2 in resources2:
            if resources_overlap(r1, r2):
                if r1.lower() == r2.lower():
                    overlaps.append(r1)
                else:
                    overlaps.append(f"{r1} <-> {r2}")
    return overlaps


def statements_are_identical(stmt1: SCPStatement, stmt2: SCPStatement) -> bool:
    """
    Check if two statements are functionally identical.

    Compares Effect, Actions/NotActions, Resources/NotResources, and Conditions.
    Ignores Sid since that's just an identifier.
    """
    if stmt1.effect != stmt2.effect:
        return False

    # Check Action vs NotAction usage matches
    if stmt1.uses_not_action != stmt2.uses_not_action:
        return False

    # Compare effective actions
    actions1 = sorted(a.lower() for a in stmt1.effective_actions)
    actions2 = sorted(a.lower() for a in stmt2.effective_actions)
    if actions1 != actions2:
        return False

    # Check Resource vs NotResource usage matches
    if stmt1.uses_not_resource != stmt2.uses_not_resource:
        return False

    # Compare effective resources
    resources1 = sorted(r.lower() for r in stmt1.effective_resources)
    resources2 = sorted(r.lower() for r in stmt2.effective_resources)
    if resources1 != resources2:
        return False

    if stmt1.conditions != stmt2.conditions:
        return False

    return True


def find_overlapping_actions(actions1: list[str], actions2: list[str]) -> list[str]:
    """Find actions that overlap between two lists."""
    overlaps = []
    for a1 in actions1:
        for a2 in actions2:
            if actions_overlap(a1, a2):
                if a1.lower() == a2.lower():
                    overlaps.append(a1)
                else:
                    overlaps.append(f"{a1} <-> {a2}")
    return overlaps


class PolicyAnalyzer:
    """
    Analyzes SCP policies for conflicts, duplicates, and shadows.

    Can analyze a single policy or multiple policies together.
    """

    def analyze(self, *policies: SCPPolicy) -> AnalysisReport:
        """
        Analyze one or more policies for issues.

        Args:
            *policies: One or more SCPPolicy objects to analyze

        Returns:
            AnalysisReport with all detected issues
        """
        report = AnalysisReport()
        report.total_policies = len(policies)

        # Collect all statements with their source info
        all_statements: list[tuple[SCPStatement, int, str | None]] = []
        for policy in policies:
            for idx, stmt in enumerate(policy.statements):
                all_statements.append((stmt, idx, policy.name))

        report.total_statements = len(all_statements)

        # Check for issues between all pairs of statements
        for i, (stmt1, idx1, policy1) in enumerate(all_statements):
            for j, (stmt2, idx2, policy2) in enumerate(all_statements):
                if i >= j:
                    continue

                stmt1_id = stmt1.sid or f"Statement[{idx1}]"
                stmt2_id = stmt2.sid or f"Statement[{idx2}]"

                # Check for duplicate Sids
                if stmt1.sid and stmt2.sid and stmt1.sid == stmt2.sid:
                    report.issues.append(
                        Issue(
                            type=IssueType.DUPLICATE_SID,
                            message=f"Duplicate Sid: '{stmt1.sid}'",
                            policy1=policy1,
                            statement1=stmt1_id,
                            policy2=policy2,
                            statement2=stmt2_id,
                        )
                    )

                # Check for identical statements
                if statements_are_identical(stmt1, stmt2):
                    report.issues.append(
                        Issue(
                            type=IssueType.DUPLICATE_STATEMENT,
                            message="Identical statements",
                            policy1=policy1,
                            statement1=stmt1_id,
                            policy2=policy2,
                            statement2=stmt2_id,
                            actions=stmt1.actions,
                        )
                    )
                    continue

                # Check for conflicts (Allow vs Deny with overlapping actions AND resources)
                if stmt1.effect != stmt2.effect:
                    # Get effective actions (handling NotAction)
                    action_overlaps = self._find_action_overlaps(stmt1, stmt2)

                    # Check resource overlap
                    resources1 = stmt1.effective_resources
                    resources2 = stmt2.effective_resources
                    resource_overlaps = find_overlapping_resources(resources1, resources2)

                    # Check condition overlap
                    cond_overlap = conditions_could_overlap(stmt1.conditions, stmt2.conditions)

                    if action_overlaps and resource_overlaps and cond_overlap:
                        allow_id = stmt1_id if stmt1.effect == "Allow" else stmt2_id
                        deny_id = stmt1_id if stmt1.effect == "Deny" else stmt2_id
                        allow_policy = policy1 if stmt1.effect == "Allow" else policy2
                        deny_policy = policy1 if stmt1.effect == "Deny" else policy2

                        report.issues.append(
                            Issue(
                                type=IssueType.CONFLICT,
                                message="Allow and Deny statements have overlapping actions",
                                policy1=allow_policy,
                                statement1=allow_id,
                                policy2=deny_policy,
                                statement2=deny_id,
                                actions=action_overlaps,
                            )
                        )

                # Check for shadows (same effect, one covers the other)
                if stmt1.effect == stmt2.effect:
                    self._check_shadow(stmt1, stmt1_id, policy1, stmt2, stmt2_id, policy2, report)

        # Check for unreachable Allow statements
        self._check_unreachable_allows(all_statements, report)

        return report

    def _find_action_overlaps(
        self,
        stmt1: SCPStatement,
        stmt2: SCPStatement,
    ) -> list[str]:
        """Find overlapping actions between two statements, handling NotAction."""
        # Simple case: both use Action
        if not stmt1.uses_not_action and not stmt2.uses_not_action:
            return find_overlapping_actions(stmt1.actions, stmt2.actions)

        # If one uses NotAction, the logic is more complex
        if stmt1.uses_not_action and stmt2.uses_not_action:
            # Both use NotAction - they overlap on everything except both exclusions
            return ["* (both use NotAction)"]

        if stmt1.uses_not_action:
            # stmt1 covers everything except not_actions
            overlaps = []
            for a2 in stmt2.actions:
                excluded = any(action_is_subset(a2, na) for na in stmt1.not_actions or [])
                if not excluded:
                    overlaps.append(a2)
            return overlaps

        if stmt2.uses_not_action:
            # stmt2 covers everything except not_actions
            overlaps = []
            for a1 in stmt1.actions:
                excluded = any(action_is_subset(a1, na) for na in stmt2.not_actions or [])
                if not excluded:
                    overlaps.append(a1)
            return overlaps

        return []

    def _check_shadow(
        self,
        stmt1: SCPStatement,
        stmt1_id: str,
        policy1: str | None,
        stmt2: SCPStatement,
        stmt2_id: str,
        policy2: str | None,
        report: AnalysisReport,
    ) -> None:
        """Check if one statement shadows another (makes it irrelevant)."""
        # Skip if using NotAction (complex to analyze shadows with NotAction)
        if stmt1.uses_not_action or stmt2.uses_not_action:
            return

        # Check if stmt1's actions are all subsets of stmt2's actions
        stmt1_actions_shadowed = all(
            any(action_is_subset(a1, a2) for a2 in stmt2.actions) for a1 in stmt1.actions
        )

        # Check if stmt1's resources are all subsets of stmt2's resources
        stmt1_resources_shadowed = all(
            any(resource_is_subset(r1, r2) for r2 in stmt2.effective_resources)
            for r1 in stmt1.effective_resources
        )

        # Check if stmt2's condition is broader (subset means more restrictive)
        stmt2_cond_broader = condition_is_subset(stmt1.conditions, stmt2.conditions)

        stmt1_shadowed = stmt1_actions_shadowed and stmt1_resources_shadowed and stmt2_cond_broader

        # Check the reverse
        stmt2_actions_shadowed = all(
            any(action_is_subset(a2, a1) for a1 in stmt1.actions) for a2 in stmt2.actions
        )

        stmt2_resources_shadowed = all(
            any(resource_is_subset(r2, r1) for r1 in stmt1.effective_resources)
            for r2 in stmt2.effective_resources
        )

        stmt1_cond_broader = condition_is_subset(stmt2.conditions, stmt1.conditions)

        stmt2_shadowed = stmt2_actions_shadowed and stmt2_resources_shadowed and stmt1_cond_broader

        if stmt1_shadowed and not stmt2_shadowed:
            report.issues.append(
                Issue(
                    type=IssueType.SHADOW,
                    message="Statement is shadowed by a broader statement",
                    policy1=policy1,
                    statement1=stmt1_id,
                    policy2=policy2,
                    statement2=stmt2_id,
                    actions=stmt1.actions,
                )
            )
        elif stmt2_shadowed and not stmt1_shadowed:
            report.issues.append(
                Issue(
                    type=IssueType.SHADOW,
                    message="Statement is shadowed by a broader statement",
                    policy1=policy2,
                    statement1=stmt2_id,
                    policy2=policy1,
                    statement2=stmt1_id,
                    actions=stmt2.actions,
                )
            )

    def _check_unreachable_allows(
        self,
        all_statements: list[tuple[SCPStatement, int, str | None]],
        report: AnalysisReport,
    ) -> None:
        """Check for Allow statements that can never apply due to Deny statements."""
        allow_stmts = [(s, i, p) for s, i, p in all_statements if s.effect == "Allow"]
        deny_stmts = [(s, i, p) for s, i, p in all_statements if s.effect == "Deny"]

        for allow_stmt, allow_idx, allow_policy in allow_stmts:
            allow_id = allow_stmt.sid or f"Statement[{allow_idx}]"

            # Skip if Allow uses NotAction (complex to analyze)
            if allow_stmt.uses_not_action:
                continue

            for deny_stmt, deny_idx, deny_policy in deny_stmts:
                deny_id = deny_stmt.sid or f"Statement[{deny_idx}]"

                # Skip if Deny uses NotAction
                if deny_stmt.uses_not_action:
                    continue

                # Check if all Allow actions are covered by Deny actions
                all_actions_covered = all(
                    any(
                        action_is_subset(allow_action, deny_action)
                        for deny_action in deny_stmt.actions
                    )
                    for allow_action in allow_stmt.actions
                )

                # Check if all Allow resources are covered by Deny resources
                all_resources_covered = all(
                    any(
                        resource_is_subset(allow_res, deny_res)
                        for deny_res in deny_stmt.effective_resources
                    )
                    for allow_res in allow_stmt.effective_resources
                )

                # Check if Deny condition is broader than or equal to Allow condition
                deny_cond_broader = condition_is_subset(allow_stmt.conditions, deny_stmt.conditions)

                if all_actions_covered and all_resources_covered and deny_cond_broader:
                    report.issues.append(
                        Issue(
                            type=IssueType.UNREACHABLE,
                            message="Allow statement is unreachable due to broader Deny",
                            policy1=allow_policy,
                            statement1=allow_id,
                            policy2=deny_policy,
                            statement2=deny_id,
                            actions=allow_stmt.actions,
                        )
                    )
                    break


def analyze_policies(*policies: SCPPolicy) -> AnalysisReport:
    """
    Convenience function to analyze multiple policies.

    Args:
        *policies: One or more SCPPolicy objects to analyze

    Returns:
        AnalysisReport with all detected issues
    """
    analyzer = PolicyAnalyzer()
    return analyzer.analyze(*policies)


def find_duplicates(*policies: SCPPolicy) -> list[Issue]:
    """Find duplicate statements and Sids across policies."""
    report = analyze_policies(*policies)
    return report.duplicates


def find_conflicts(*policies: SCPPolicy) -> list[Issue]:
    """Find Allow/Deny conflicts across policies."""
    report = analyze_policies(*policies)
    return report.conflicts


def find_shadows(*policies: SCPPolicy) -> list[Issue]:
    """Find shadowed statements across policies."""
    report = analyze_policies(*policies)
    return report.shadows


def find_unreachable(*policies: SCPPolicy) -> list[Issue]:
    """Find unreachable Allow statements across policies."""
    report = analyze_policies(*policies)
    return report.unreachable


def merge_policies(*policies: SCPPolicy, merged_name: str = "MergedPolicy") -> SCPPolicy:
    """
    Merge multiple SCP policies into a single policy.

    Note: This creates a combined policy for analysis purposes.
    The resulting policy may exceed AWS size limits.

    Args:
        *policies: Policies to merge
        merged_name: Name for the merged policy

    Returns:
        New SCPPolicy containing all statements from input policies
    """
    if not policies:
        raise ValueError("At least one policy is required")

    all_statements = []
    for policy in policies:
        all_statements.extend(policy.statements)

    return SCPPolicy(
        statements=all_statements,
        name=merged_name,
        version="2012-10-17",
    )


def analyze_policy_addition(
    existing_policy: SCPPolicy,
    new_statements: list[SCPStatement],
) -> AnalysisReport:
    """
    Analyze what happens when adding new statements to an existing policy.

    This is the key use case: "What conflicts arise if I add this statement?"

    Args:
        existing_policy: The current policy
        new_statements: Statements to be added

    Returns:
        Analysis report focusing on issues between existing and new statements
    """
    analyzer = PolicyAnalyzer()

    # Create a temporary policy with the new statements
    new_policy = SCPPolicy(
        statements=new_statements,
        name="NewStatements",
        version="2012-10-17",
    )

    # Analyze both together
    return analyzer.analyze(existing_policy, new_policy)
