"""
Resource ARN extractor for CloudTrail events.

Uses a multi-layer extraction strategy to resolve resource ARNs from
CloudTrail event data, falling back through increasingly heuristic methods.
"""

from __future__ import annotations

import re
from typing import Any

from ..data.iam_reference import get_iam_reference
from ..models.cloudtrail import CloudTrailEvent
from .quirky_service_handlers import QUIRKY_SERVICES as _QUIRKY_SERVICES
from .quirky_service_handlers import extract_from_quirky_service

# Keys in requestParameters that commonly contain ARN values
KNOWN_ARN_PARAM_KEYS: list[str] = [
    # Generic ARN keys
    "resourceArn",
    "ResourceArn",
    "arn",
    "Arn",
    "targetArn",
    "TargetArn",
    "sourceArn",
    "SourceArn",
    "destinationArn",
    "DestinationArn",
    # SNS
    "topicArn",
    "TopicArn",
    "snsTopicArn",
    "SnsTopicArn",
    "notificationArn",
    "NotificationArn",
    # SQS
    "queueArn",
    "QueueArn",
    "sqsQueueArn",
    "SqsQueueArn",
    "deadLetterTargetArn",
    "DeadLetterTargetArn",
    # DynamoDB
    "tableArn",
    "TableArn",
    "globalTableArn",
    "GlobalTableArn",
    # ECS/EKS
    "clusterArn",
    "ClusterArn",
    "taskArn",
    "TaskArn",
    "containerInstanceArn",
    "ContainerInstanceArn",
    # Secrets Manager
    "secretArn",
    "SecretArn",
    # SQS (URL-based)
    "queueUrl",
    "QueueUrl",
    # Kinesis
    "streamArn",
    "StreamArn",
    "streamARN",
    "StreamARN",
    # ACM
    "certificateArn",
    "CertificateArn",
    # Elasticsearch/OpenSearch
    "domainArn",
    "DomainArn",
    # CloudWatch Logs
    "logGroupArn",
    "LogGroupArn",
    "logStreamArn",
    "LogStreamArn",
    # Lambda
    "functionArn",
    "FunctionArn",
    "layerArn",
    "LayerArn",
    "eventSourceArn",
    "EventSourceArn",
    # Step Functions
    "executionArn",
    "ExecutionArn",
    "stateMachineArn",
    "StateMachineArn",
    "activityArn",
    "ActivityArn",
    # ECR
    "repositoryArn",
    "RepositoryArn",
    "registryArn",
    "RegistryArn",
    "imageArn",
    "ImageArn",
    # ECS
    "taskDefinitionArn",
    "TaskDefinitionArn",
    "serviceArn",
    "ServiceArn",
    # RDS
    "dbInstanceArn",
    "DBInstanceArn",
    "dBInstanceArn",
    "dbClusterArn",
    "DBClusterArn",
    "dBClusterArn",
    "dbSnapshotArn",
    "DBSnapshotArn",
    # CloudFront
    "distributionArn",
    "DistributionArn",
    # Route 53
    "hostedZoneId",
    "HostedZoneId",
    # API Gateway
    "restApiId",
    "RestApiId",
    "apiId",
    "ApiId",
    # KMS
    "kmsKeyArn",
    "KmsKeyArn",
    "keyArn",
    "KeyArn",
    "masterKeyArn",
    "MasterKeyArn",
    # IAM
    "roleArn",
    "RoleArn",
    "policyArn",
    "PolicyArn",
    "instanceProfileArn",
    "InstanceProfileArn",
    # CodeBuild/CodePipeline
    "projectArn",
    "ProjectArn",
    "pipelineArn",
    "PipelineArn",
    "buildArn",
    "BuildArn",
    # CloudFormation
    "stackArn",
    "StackArn",
    "stackSetArn",
    "StackSetArn",
    # EventBridge
    "eventBusArn",
    "EventBusArn",
    "ruleArn",
    "RuleArn",
    # Firehose
    "deliveryStreamArn",
    "DeliveryStreamArn",
    # Glue
    "catalogArn",
    "CatalogArn",
    "crawlerArn",
    "CrawlerArn",
    # Backup
    "backupVaultArn",
    "BackupVaultArn",
    "recoveryPointArn",
    "RecoveryPointArn",
    # EKS
    "nodegroupArn",
    "NodegroupArn",
    "fargateProfileArn",
    "FargateProfileArn",
    # ELBv2
    "loadBalancerArn",
    "LoadBalancerArn",
    "targetGroupArn",
    "TargetGroupArn",
    "listenerArn",
    "ListenerArn",
    # CloudWatch
    "alarmArn",
    "AlarmArn",
    # Redshift
    "clusterSnapshotArn",
    "ClusterSnapshotArn",
    # SageMaker
    "notebookInstanceArn",
    "NotebookInstanceArn",
    "trainingJobArn",
    "TrainingJobArn",
    "endpointArn",
    "EndpointArn",
    "modelArn",
    "ModelArn",
    # Organizations
    "policyId",
    "PolicyId",
    # Athena
    "workGroupArn",
    "WorkGroupArn",
    "dataCatalogArn",
    "DataCatalogArn",
    # CloudTrail
    "trailArn",
    "TrailArn",
    "eventDataStoreArn",
    "EventDataStoreArn",
    # Config
    "configRuleArn",
    "ConfigRuleArn",
    "deliveryChannelArn",
    "DeliveryChannelArn",
    # GuardDuty
    "detectorArn",
    "DetectorArn",
    # Access Analyzer
    "analyzerArn",
    "AnalyzerArn",
    # WAFv2
    "webACLArn",
    "WebACLArn",
    "ruleGroupArn",
    "RuleGroupArn",
    "ipSetArn",
    "IpSetArn",
    # Cognito
    "userPoolArn",
    "UserPoolArn",
    "identityPoolArn",
    "IdentityPoolArn",
    # AppSync
    "graphqlApiArn",
    "GraphqlApiArn",
    # Security Hub
    "hubArn",
    "HubArn",
    "productArn",
    "ProductArn",
    # Backup
    "backupPlanArn",
    "BackupPlanArn",
]

# ARN pattern regex
_ARN_REGEX = re.compile(r"^arn:[\w\-\*]+:[\w\-\*]+:[\w\-\*]*:\d*:[\w\-\*\./:+=@]+$")

# Placeholder pattern in IAM reference ARN templates
_PLACEHOLDER_REGEX = re.compile(r"\$\{(\w+)\}")

# Maps IAM reference placeholder names to CloudTrail requestParameter keys
# for cases where the naming conventions diverge and heuristics don't work.
_PARAM_ALIASES: dict[str, list[str]] = {
    # RDS: IAM uses DbInstanceName but CloudTrail uses dBInstanceIdentifier
    "DbInstanceName": ["dBInstanceIdentifier", "DBInstanceIdentifier"],
    "DbClusterInstanceName": ["dBClusterIdentifier", "DBClusterIdentifier"],
    # Redshift: CloudTrail uses clusterIdentifier not clusterName
    "ClusterName": ["clusterIdentifier", "ClusterIdentifier"],
    # Config: IAM uses ConfigRuleId but CloudTrail uses configRuleName
    "ConfigRuleId": ["configRuleName", "ConfigRuleName"],
    # States: CloudTrail uses generic name param
    "StateMachineName": ["name", "Name", "stateMachineName"],
    # Athena: CloudTrail uses catalogName not dataCatalogName
    "DataCatalogName": ["catalogName", "CatalogName"],
    # CodePipeline: CloudTrail uses generic name param
    "PipelineName": ["name", "Name", "pipelineName"],
}


class ResourceExtractor:
    """
    Extracts resource ARNs from CloudTrail events using a multi-layer strategy.

    Layers (tried in order):
    1. Direct `resources` field from the event
    2. Hardcoded service-specific patterns (S3, EC2, IAM, Lambda)
    3. Known ARN parameter keys in requestParameters
    4. IAM reference pattern resolution using service resource definitions
    5. Response elements scan for ARN strings
    """

    def __init__(self) -> None:
        self._iam_ref = get_iam_reference()

    def extract(self, event: CloudTrailEvent) -> str | None:
        """
        Extract resource ARN from a CloudTrail event.

        Tries each extraction layer in order, returning the first
        successfully resolved ARN.

        Args:
            event: CloudTrail event to extract resource ARN from.

        Returns:
            Resource ARN string or None if no ARN could be resolved.
        """
        # Layer 1: Direct resources field
        arn = self._from_resources_field(event)
        if arn:
            return arn

        # Layer 2: Hardcoded service patterns
        arn = self._from_service_patterns(event)
        if arn:
            return arn

        # Layer 3: Known ARN parameter keys
        arn = self._from_arn_param_keys(event)
        if arn:
            return arn

        # Layer 4: IAM reference pattern resolution
        arn = self._from_iam_reference(event)
        if arn:
            return arn

        # Layer 5: Response elements scan
        arn = self._from_response_elements(event)
        if arn:
            return arn

        return None

    def _from_resources_field(self, event: CloudTrailEvent) -> str | None:
        """Layer 1: Extract ARN from event.resources field."""
        if not event.resources:
            return None
        for resource in event.resources:
            if isinstance(resource, dict):
                arn = resource.get("ARN") or resource.get("arn")
                if arn and isinstance(arn, str):
                    return str(arn)
        return None

    def _from_service_patterns(self, event: CloudTrailEvent) -> str | None:
        """Layer 2: Hardcoded patterns for services with quirky extraction logic.

        Only services in QUIRKY_SERVICES are handled here.  All other services
        are resolved by the data-driven Layer 4 (IAM reference patterns).
        Handlers live in quirky_service_handlers.py.
        """
        service = event.event_source.split(".")[0]
        if service not in _QUIRKY_SERVICES:
            return None
        region = event.aws_region or "*"
        account = event.account_id or "*"
        return extract_from_quirky_service(event, region, account)

    def _from_arn_param_keys(self, event: CloudTrailEvent) -> str | None:
        """Layer 3: Scan requestParameters for known ARN keys."""
        if not event.request_parameters:
            return None

        params = event.request_parameters
        for key in KNOWN_ARN_PARAM_KEYS:
            value = params.get(key)
            if isinstance(value, str) and value.startswith("arn:"):
                return value

        return None

    def _from_iam_reference(self, event: CloudTrailEvent) -> str | None:
        """Layer 4: Resolve ARN using IAM reference resource patterns.

        Enhanced data-driven resolution that handles all non-quirky services.
        Features:
        - ARN passthrough: detects param values that are already valid ARNs
        - Best-match scoring: picks the most specific resolved pattern
        - Regionless/accountless: handled natively by IAM reference patterns
        """
        if not event.request_parameters:
            return None

        service_prefix = event.event_source.split(".")[0]

        # Quirky services are fully handled by Layer 2
        if service_prefix in _QUIRKY_SERVICES:
            return None

        params = event.request_parameters

        # ARN passthrough: check if any param value is already a valid ARN
        # for this service (e.g. functionName="arn:aws:lambda:...")
        arn = self._find_arn_passthrough(params, service_prefix)
        if arn:
            return arn

        service = self._iam_ref.get_service(service_prefix)
        if not service or not service.resources:
            return None

        region = event.aws_region or "*"
        account_id = event.account_id or "*"

        # Try all resource patterns and pick the best match.
        # Score = (placeholders_resolved, -path_depth).
        # More resolved placeholders = better match.
        # At same placeholder count, fewer path segments (/) = more general
        # base resource, which is preferred over sub-resources.
        # At same score, first match wins (preserves IAM reference ordering).
        best_arn: str | None = None
        best_score: tuple[int, int] = (-1, 0)

        for resource_info in service.resources.values():
            pattern = resource_info.arn_pattern
            if not pattern:
                continue

            resolved = self._resolve_pattern(pattern, params, region, account_id)
            if resolved:
                stripped = pattern.replace("${Partition}", "")
                stripped = stripped.replace("${Region}", "")
                stripped = stripped.replace("${Account}", "")
                n_placeholders = len(_PLACEHOLDER_REGEX.findall(stripped))
                n_slashes = pattern.count("/")
                score = (n_placeholders, -n_slashes)
                if score > best_score:
                    best_score = score
                    best_arn = resolved

        return best_arn

    def _find_arn_passthrough(
        self, params: dict[str, Any], service_prefix: str
    ) -> str | None:
        """Check if any parameter value is already a valid ARN for this service."""
        for value in params.values():
            if not isinstance(value, str) or not value.startswith("arn:"):
                continue
            # Verify the ARN belongs to this service
            # ARN format: arn:partition:service:region:account:resource
            parts = value.split(":", 5)
            if len(parts) >= 3 and parts[2] == service_prefix:
                return value
        return None

    def _resolve_pattern(
        self,
        pattern: str,
        params: dict[str, Any],
        region: str,
        account_id: str,
    ) -> str | None:
        """
        Resolve an IAM reference ARN pattern using event parameters.

        Replaces template placeholders like ${TableName} with values from
        requestParameters, using multiple name matching strategies.

        Args:
            pattern: IAM ARN pattern with ${Placeholder} variables
            params: requestParameters from the CloudTrail event
            region: AWS region
            account_id: AWS account ID

        Returns:
            Resolved ARN string, or None if any placeholder can't be filled.
        """
        # Standard replacements
        result = pattern.replace("${Partition}", "aws")
        result = result.replace("${Region}", region)
        result = result.replace("${Account}", account_id)

        # Find remaining placeholders
        remaining = _PLACEHOLDER_REGEX.findall(result)
        if not remaining:
            return result

        for placeholder in remaining:
            value = self._find_param_value(placeholder, params)
            if value is None:
                return None
            if not isinstance(value, str):
                value = str(value)
            result = result.replace(f"${{{placeholder}}}", value)

        return result

    def _find_param_value(self, placeholder: str, params: dict[str, Any]) -> Any | None:
        """
        Find a parameter value using multiple matching strategies.

        Tries in order:
        1. Exact match (TableName)
        2. camelCase (tableName)
        3. lowercase (tablename)
        4. snake_case (table_name)
        5. Abbreviation expansion (Db -> Database, Sg -> SecurityGroup)

        Args:
            placeholder: The placeholder name from the ARN pattern
            params: requestParameters from the CloudTrail event

        Returns:
            Parameter value or None if not found.
        """
        # 1. Exact match
        if placeholder in params:
            return params[placeholder]

        # 2. camelCase: "TableName" -> "tableName"
        camel = placeholder[0].lower() + placeholder[1:] if placeholder else ""
        if camel in params:
            return params[camel]

        # 3. lowercase
        lower = placeholder.lower()
        if lower in params:
            return params[lower]

        # 4. snake_case: "TableName" -> "table_name"
        snake = self._to_snake_case(placeholder)
        if snake in params:
            return params[snake]

        # 5. Abbreviation expansion
        expanded = self._expand_abbreviations(placeholder)
        if expanded != placeholder:
            # Try the expanded form with all strategies
            if expanded in params:
                return params[expanded]
            expanded_camel = expanded[0].lower() + expanded[1:] if expanded else ""
            if expanded_camel in params:
                return params[expanded_camel]
            expanded_lower = expanded.lower()
            if expanded_lower in params:
                return params[expanded_lower]

        # 6. Suffix stripping: try without common suffixes (Name, Id, Identifier)
        for suffix in ("Name", "Id", "Identifier"):
            if placeholder.endswith(suffix) and len(placeholder) > len(suffix):
                stem = placeholder[: -len(suffix)]
                if stem in params:
                    return params[stem]
                stem_camel = stem[0].lower() + stem[1:] if stem else ""
                if stem_camel in params:
                    return params[stem_camel]
                stem_lower = stem.lower()
                if stem_lower in params:
                    return params[stem_lower]

        # 7. Explicit param aliases for known placeholder-to-param mismatches
        aliases = _PARAM_ALIASES.get(placeholder)
        if aliases:
            for alias in aliases:
                if alias in params:
                    return params[alias]

        # 8. Generic "name"/"Name" fallback for placeholders ending in "Name"
        # Many services use a bare "name" param (e.g. CodeBuild, States)
        if placeholder.endswith("Name") and placeholder != "Name":
            name_val = params.get("name") or params.get("Name")
            if name_val is not None:
                return name_val

        return None

    def _to_snake_case(self, name: str) -> str:
        """Convert PascalCase/camelCase to snake_case."""
        result = []
        for i, char in enumerate(name):
            if char.isupper() and i > 0:
                result.append("_")
            result.append(char.lower())
        return "".join(result)

    def _expand_abbreviations(self, name: str) -> str:
        """Expand common AWS abbreviations in parameter names."""
        abbreviations = {
            "Db": "Database",
            "DB": "Database",
            "Sg": "SecurityGroup",
            "SG": "SecurityGroup",
            "Vpc": "VPC",
            "Elb": "LoadBalancer",
            "ELB": "LoadBalancer",
            "Alb": "ApplicationLoadBalancer",
            "ALB": "ApplicationLoadBalancer",
            "Nlb": "NetworkLoadBalancer",
            "NLB": "NetworkLoadBalancer",
            "Asg": "AutoScalingGroup",
            "ASG": "AutoScalingGroup",
            "Iam": "IAM",
            "Ec2": "EC2",
            "S3": "S3",
            "Sns": "SNS",
            "Sqs": "SQS",
            "Rds": "RDS",
            "Ecs": "ECS",
            "Eks": "EKS",
            "Ecr": "ECR",
        }

        result = name
        for abbrev, full in abbreviations.items():
            if abbrev in result:
                result = result.replace(abbrev, full)
        return result

    def _from_response_elements(self, event: CloudTrailEvent) -> str | None:
        """Layer 5: Recursively scan responseElements for ARN strings."""
        if not event.response_elements:
            return None
        return self._scan_for_arn(event.response_elements, max_depth=3)

    def _scan_for_arn(self, obj: Any, max_depth: int) -> str | None:
        """Recursively search a nested structure for an ARN string."""
        if max_depth <= 0:
            return None

        if isinstance(obj, str):
            if _ARN_REGEX.match(obj):
                return obj
            return None

        if isinstance(obj, dict):
            # Prefer keys that hint at ARN values
            for key in sorted(obj.keys(), key=lambda k: 0 if "arn" in k.lower() else 1):
                result = self._scan_for_arn(obj[key], max_depth - 1)
                if result:
                    return result

        if isinstance(obj, list):
            for item in obj:
                result = self._scan_for_arn(item, max_depth - 1)
                if result:
                    return result

        return None
