"""Action and resource matching logic for SCPs."""

import fnmatch
import re


def action_matches(scp_action: str, cloudtrail_action: str) -> bool:
    """
    Check if a CloudTrail action matches an SCP action pattern.

    SCPs support wildcards in actions:
    - "*" matches any action
    - "service:*" matches all actions in a service (e.g., "s3:*")
    - "service:Action*" matches actions with prefix (e.g., "ec2:Describe*")

    Examples:
        action_matches("s3:*", "s3:GetObject") -> True
        action_matches("ec2:Describe*", "ec2:DescribeInstances") -> True
        action_matches("*", "s3:GetObject") -> True
        action_matches("s3:GetObject", "s3:GetObject") -> True
        action_matches("s3:Get*", "s3:PutObject") -> False

    Args:
        scp_action: Action pattern from SCP (may contain wildcards)
        cloudtrail_action: Actual action from CloudTrail event

    Returns:
        True if the CloudTrail action matches the SCP pattern
    """
    # Normalize to lowercase for case-insensitive matching
    scp_action = scp_action.lower()
    cloudtrail_action = cloudtrail_action.lower()

    # Exact match
    if scp_action == cloudtrail_action:
        return True

    # Universal wildcard
    if scp_action == "*":
        return True

    # Service-level wildcard (e.g., "s3:*")
    if scp_action.endswith(":*"):
        scp_service = scp_action.split(":")[0]
        if ":" in cloudtrail_action:
            ct_service = cloudtrail_action.split(":")[0]
            return scp_service == ct_service
        return False

    # Action prefix wildcard (e.g., "ec2:Describe*")
    if "*" in scp_action:
        return fnmatch.fnmatch(cloudtrail_action, scp_action)

    return False


def action_matches_any(scp_actions: list[str], cloudtrail_action: str) -> bool:
    """
    Check if a CloudTrail action matches any of the SCP actions.

    Args:
        scp_actions: List of action patterns from SCP
        cloudtrail_action: Actual action from CloudTrail event

    Returns:
        True if the CloudTrail action matches any SCP pattern
    """
    return any(action_matches(scp_action, cloudtrail_action) for scp_action in scp_actions)


def action_excluded_by_not_action(not_actions: list[str], cloudtrail_action: str) -> bool:
    """
    Check if a CloudTrail action is excluded by a NotAction list.

    NotAction means "all actions EXCEPT these" - so if the action matches
    a NotAction pattern, it is NOT included in the statement's scope.

    Args:
        not_actions: List of action patterns to exclude
        cloudtrail_action: Actual action from CloudTrail event

    Returns:
        True if the action is excluded (matches one of the NotAction patterns)
    """
    return action_matches_any(not_actions, cloudtrail_action)


def extract_service(action: str) -> str:
    """
    Extract the service name from an IAM action.

    Examples:
        extract_service("s3:GetObject") -> "s3"
        extract_service("ec2:RunInstances") -> "ec2"
        extract_service("iam:CreateUser") -> "iam"

    Args:
        action: IAM action in format "service:Action"

    Returns:
        Service name
    """
    if ":" in action:
        return action.split(":")[0].lower()
    return action.lower()


def _arn_pattern_matches(pattern: str, value: str) -> bool:
    """Check if a value matches an ARN/wildcard pattern (case-insensitive).

    Shared logic for resource and principal matching. Handles exact match,
    regex-based wildcard matching, and fnmatch fallback.

    Args:
        pattern: Pattern that may contain ``*`` wildcards.
        value: The actual ARN or identifier to test.

    Returns:
        True if *value* matches *pattern*.
    """
    pattern_lower = pattern.lower()
    value_lower = value.lower()

    if pattern_lower == value_lower:
        return True

    regex_pattern = re.escape(pattern_lower).replace(r"\*", ".*")
    regex_pattern = f"^{regex_pattern}$"

    try:
        return bool(re.match(regex_pattern, value_lower))
    except re.error:
        return fnmatch.fnmatch(value_lower, pattern_lower)


def resource_matches(pattern: str, resource_arn: str | None) -> bool:
    """
    Check if a resource ARN matches a resource pattern.

    Patterns can include wildcards:
    - "*" matches any resource
    - "arn:aws:s3:::*" matches any S3 bucket
    - "arn:aws:s3:::bucket/*" matches any object in bucket
    - "arn:aws:s3:::bucket/prefix*" matches objects with prefix

    Examples:
        resource_matches("*", "arn:aws:s3:::bucket/key") -> True
        resource_matches("arn:aws:s3:::*", "arn:aws:s3:::mybucket") -> True
        resource_matches("arn:aws:s3:::mybucket/*", "arn:aws:s3:::mybucket/file.txt") -> True
        resource_matches("arn:aws:s3:::otherbucket/*", "arn:aws:s3:::mybucket/file.txt") -> False

    Args:
        pattern: Resource pattern from SCP (may contain wildcards)
        resource_arn: Actual resource ARN from CloudTrail event

    Returns:
        True if the resource ARN matches the pattern
    """
    if pattern == "*":
        return True
    if resource_arn is None:
        return pattern == "*"
    return _arn_pattern_matches(pattern, resource_arn)


def resource_matches_any(patterns: list[str], resource_arn: str | None) -> bool:
    """
    Check if a resource ARN matches any of the given patterns.

    Args:
        patterns: List of resource patterns from SCP
        resource_arn: Actual resource ARN from CloudTrail event

    Returns:
        True if the resource ARN matches any pattern
    """
    return any(resource_matches(pattern, resource_arn) for pattern in patterns)


def resource_excluded_by_not_resource(not_resources: list[str], resource_arn: str | None) -> bool:
    """
    Check if a resource ARN is excluded by a NotResource list.

    NotResource means "all resources EXCEPT these" - so if the resource matches
    a NotResource pattern, it is NOT included in the statement's scope.

    Args:
        not_resources: List of resource patterns to exclude
        resource_arn: Actual resource ARN from CloudTrail event

    Returns:
        True if the resource is excluded (matches one of the NotResource patterns)
    """
    return resource_matches_any(not_resources, resource_arn)


def principal_matches(pattern: str, principal_arn: str | None) -> bool:
    """
    Check if a principal ARN matches a principal pattern.

    Patterns can include:
    - "*" matches any principal
    - Specific ARN patterns with wildcards
    - Account IDs (e.g., "123456789012")

    Examples:
        principal_matches("*", "arn:aws:iam::123:user/admin") -> True
        principal_matches("arn:aws:iam::123:*", "arn:aws:iam::123:user/admin") -> True
        principal_matches("arn:aws:iam::123:role/*", "arn:aws:iam::123:role/Admin") -> True

    Args:
        pattern: Principal pattern from SCP (may contain wildcards)
        principal_arn: Actual principal ARN from CloudTrail event

    Returns:
        True if the principal ARN matches the pattern
    """
    if pattern == "*":
        return True
    if principal_arn is None:
        return pattern == "*"

    # Account ID shorthand (12-digit number)
    if pattern.isdigit() and len(pattern) == 12:
        return f"::{pattern}:" in principal_arn

    return _arn_pattern_matches(pattern, principal_arn)


def principal_matches_any(patterns: list[str], principal_arn: str | None) -> bool:
    """
    Check if a principal ARN matches any of the given patterns.

    Args:
        patterns: List of principal patterns from SCP
        principal_arn: Actual principal ARN from CloudTrail event

    Returns:
        True if the principal ARN matches any pattern
    """
    return any(principal_matches(pattern, principal_arn) for pattern in patterns)


def principal_excluded_by_not_principal(
    not_principals: list[str], principal_arn: str | None
) -> bool:
    """
    Check if a principal ARN is excluded by a NotPrincipal list.

    NotPrincipal means "all principals EXCEPT these" - so if the principal matches
    a NotPrincipal pattern, it is NOT included in the statement's scope.

    Args:
        not_principals: List of principal patterns to exclude
        principal_arn: Actual principal ARN from CloudTrail event

    Returns:
        True if the principal is excluded (matches one of the NotPrincipal patterns)
    """
    return principal_matches_any(not_principals, principal_arn)
