"""Generate realistic mock CloudTrail events for SCP simulation testing.

Uses the IAM reference database (442 AWS services) to generate events with
proper requestParameters that the resource extractor can resolve back to ARNs.
"""

from __future__ import annotations

import random
import re
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any

from ..data.iam_reference import AWSIAMReference, ServiceInfo, get_iam_reference

_PLACEHOLDER_RE = re.compile(r"\$\{(\w+)\}")

# Services whose eventSource doesn't follow {prefix}.amazonaws.com
_EVENT_SOURCE_MAP: dict[str, str] = {
    "monitoring": "monitoring.amazonaws.com",
    "logs": "logs.amazonaws.com",
    "states": "states.amazonaws.com",
    "events": "events.amazonaws.com",
    "elasticloadbalancing": "elasticloadbalancing.amazonaws.com",
    "cognito-idp": "cognito-idp.amazonaws.com",
    "cognito-identity": "cognito-identity.amazonaws.com",
    "execute-api": "execute-api.amazonaws.com",
    "iot": "iot.amazonaws.com",
    "access-analyzer": "access-analyzer.amazonaws.com",
    "resource-groups": "resource-groups.amazonaws.com",
    "wafv2": "wafv2.amazonaws.com",
    "waf-regional": "waf-regional.amazonaws.com",
}

_SOURCE_IPS = [
    "203.0.113.10",
    "203.0.113.25",
    "198.51.100.42",
    "192.0.2.100",
    "10.0.1.50",
]

_USER_AGENTS = [
    "aws-cli/2.15.0 Python/3.11.6",
    "aws-sdk-python/1.34.0",
    "console.aws.amazon.com",
    "Terraform/1.7.0",
    "aws-sdk-go/1.50.0",
]


@dataclass
class GeneratorConfig:
    """Configuration for CloudTrail event generation."""

    services: list[str] | None = None
    count: int = 500
    regions: list[str] = field(
        default_factory=lambda: ["us-east-1", "us-west-2", "eu-west-1"]
    )
    account_id: str = "123456789012"
    seed: int | None = None
    write_only: bool = False


class CloudTrailGenerator:
    """Generates realistic mock CloudTrail events for all AWS services."""

    def __init__(self, config: GeneratorConfig) -> None:
        self._config = config
        self._iam_ref: AWSIAMReference = get_iam_reference()
        self._rng = random.Random(config.seed)
        self._counter = 0

    def generate(self) -> list[dict]:
        """Generate CloudTrail events according to config.

        Returns:
            List of CloudTrail event dicts ready for JSON serialization.
        """
        services = self._resolve_services()
        if not services:
            return []

        distribution = self._distribute_events(services)
        events: list[dict] = []

        for prefix, event_count in distribution.items():
            svc = self._iam_ref.get_service(prefix)
            if not svc:
                continue
            for _ in range(event_count):
                event = self._generate_event(svc)
                if event:
                    events.append(event)

        self._rng.shuffle(events)
        self._assign_timestamps(events)
        return events

    def _resolve_services(self) -> list[str]:
        """Resolve service list from config."""
        all_prefixes = self._iam_ref.service_prefixes
        if self._config.services is None:
            return all_prefixes
        valid = [s for s in self._config.services if s in all_prefixes]
        return valid

    def _distribute_events(self, services: list[str]) -> dict[str, int]:
        """Distribute total event count across services.

        Weights by action count so larger services get more events,
        but every selected service gets at least 1 event.
        """
        total = self._config.count
        if total <= 0:
            return {}

        # Get action counts per service
        weights: dict[str, int] = {}
        for prefix in services:
            svc = self._iam_ref.get_service(prefix)
            if svc:
                actions = self._get_actions(svc)
                if actions:
                    weights[prefix] = len(actions)

        if not weights:
            return {}

        # If count < number of services, sample a subset
        if total < len(weights):
            sampled = self._rng.sample(list(weights.keys()), total)
            return {s: 1 for s in sampled}

        # Give each service at least 1, distribute remainder by weight
        distribution = {s: 1 for s in weights}
        remaining = total - len(weights)

        if remaining > 0:
            total_weight = sum(weights.values())
            for prefix in weights:
                share = int(remaining * weights[prefix] / total_weight)
                distribution[prefix] += share

            # Distribute any leftover from rounding
            allocated = sum(distribution.values())
            leftover = total - allocated
            if leftover > 0:
                extras = self._rng.sample(list(weights.keys()), min(leftover, len(weights)))
                for prefix in extras:
                    distribution[prefix] += 1

        return distribution

    def _get_actions(self, svc: ServiceInfo) -> list[str]:
        """Get applicable actions for a service based on config."""
        if self._config.write_only:
            actions = []
            for level in ("write", "permissions management"):
                actions.extend(svc.get_actions_by_level(level))
            return actions
        return svc.all_actions

    def _generate_event(self, svc: ServiceInfo) -> dict | None:
        """Generate a single CloudTrail event for a service."""
        actions = self._get_actions(svc)
        if not actions:
            return None

        action = self._rng.choice(actions)
        region = self._rng.choice(self._config.regions)
        account = self._config.account_id
        event_source = self._get_event_source(svc.prefix)

        # Build requestParameters from resource ARN patterns
        request_params = self._build_request_params(svc, action, region, account)

        # Enrich with condition key parameters
        request_params = self._add_condition_key_params(svc, request_params)

        # Build userIdentity
        user_identity = self._build_user_identity(account)

        self._counter += 1
        return {
            "eventVersion": "1.08",
            "userIdentity": user_identity,
            "eventTime": "",  # Filled by _assign_timestamps
            "eventSource": event_source,
            "eventName": action,
            "awsRegion": region,
            "sourceIPAddress": self._rng.choice(_SOURCE_IPS),
            "userAgent": self._rng.choice(_USER_AGENTS),
            "requestParameters": request_params if request_params else None,
            "responseElements": None,
            "eventID": str(uuid.UUID(int=self._rng.getrandbits(128), version=4)),
            "eventType": "AwsApiCall",
            "recipientAccountId": account,
            "eventCategory": "Management",
        }

    def _get_event_source(self, prefix: str) -> str:
        """Map service prefix to CloudTrail eventSource."""
        if prefix in _EVENT_SOURCE_MAP:
            return _EVENT_SOURCE_MAP[prefix]
        return f"{prefix}.amazonaws.com"

    def _build_request_params(
        self,
        svc: ServiceInfo,
        action: str,
        region: str,
        account: str,
    ) -> dict | None:
        """Build requestParameters from IAM reference resource patterns.

        Extracts ${Placeholder} names from the first matching resource ARN
        pattern and generates fake values for each, producing parameters
        that the resource extractor can resolve.
        """
        if not svc.resources:
            return None

        # Try to find a resource pattern with resolvable placeholders
        for resource_info in svc.resources.values():
            pattern = resource_info.arn_pattern
            if not pattern:
                continue

            # Replace standard placeholders
            resolved = pattern.replace("${Partition}", "aws")
            resolved = resolved.replace("${Region}", region)
            resolved = resolved.replace("${Account}", account)

            # Find remaining placeholders that need requestParameters
            placeholders = _PLACEHOLDER_RE.findall(resolved)
            if not placeholders:
                # No service-specific placeholders — nothing to put in params
                continue

            params: dict = {}
            for ph in placeholders:
                key = self._placeholder_to_param_key(ph)
                value = self._generate_fake_value(ph, svc.prefix)
                params[key] = value
            return params

        return None

    def _add_condition_key_params(
        self,
        svc: ServiceInfo,
        params: dict | None,
    ) -> dict | None:
        """Add condition key parameters to requestParameters.

        Randomly selects 0-3 condition keys from the service and adds
        corresponding parameters with realistic fake values.  This makes
        generated events useful for testing condition evaluation, not just
        resource extraction.

        Skips:
        - Tag-based keys (contain '/' or '$')
        - Keys with nested colons (e.g., sagemaker:ModelLifeCycle:Stage)
        - Keys whose derived param already exists in params
        """
        if not svc.condition_keys:
            return params

        # Filter to eligible condition keys
        eligible: list[str] = []
        for key_name in svc.condition_keys:
            # Skip tag-based and template keys
            if "$" in key_name or "/" in key_name:
                continue
            if ":" not in key_name:
                continue
            _, key_part = key_name.split(":", 1)
            # Skip nested colon keys
            if ":" in key_part:
                continue
            eligible.append(key_name)

        if not eligible:
            return params

        # Pick 0-3 condition keys randomly
        n = min(self._rng.randint(0, 3), len(eligible))
        if n == 0:
            return params

        if params is None:
            params = {}

        selected = self._rng.sample(eligible, n)
        for key_name in selected:
            _, key_part = key_name.split(":", 1)
            # Convert to camelCase param key
            if "-" in key_part:
                parts = key_part.split("-")
                param_key = parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
            elif key_part:
                param_key = key_part[0].lower() + key_part[1:]
            else:
                continue

            # Don't overwrite existing ARN-based params
            if param_key in params:
                continue

            key_info = svc.condition_keys[key_name]
            params[param_key] = self._generate_condition_value(key_info)

        return params

    def _generate_condition_value(self, key_info: Any) -> str:
        """Generate a realistic fake value based on condition key type."""
        key_type = key_info.type.lower()

        # If the key has known values, pick one
        if key_info.values:
            result: str = self._rng.choice(key_info.values)
            return result

        if key_type == "arn":
            rid = self._rng.randint(1000, 9999)
            return f"arn:aws:service:us-east-1:123456789012:resource/{rid}"
        if "bool" in key_type:
            return self._rng.choice(["true", "false"])
        if "numeric" in key_type or "long" in key_type:
            return str(self._rng.randint(1, 100))
        if "date" in key_type:
            return "2025-06-15T12:00:00Z"

        # Default: string value
        lower_name = key_info.name.lower()
        if "type" in lower_name:
            return self._rng.choice(["standard", "advanced", "basic"])
        if "encryption" in lower_name or "encrypted" in lower_name:
            return self._rng.choice(["AES256", "aws:kms"])
        if "access" in lower_name:
            return self._rng.choice(["Enabled", "Disabled"])
        if "kms" in lower_name:
            kid = uuid.UUID(int=self._rng.getrandbits(128), version=4)
            return f"arn:aws:kms:us-east-1:123456789012:key/{kid}"

        return f"value-{self._rng.randint(1000, 9999)}"

    def _placeholder_to_param_key(self, placeholder: str) -> str:
        """Convert an ARN placeholder name to a camelCase parameter key.

        Examples:
            TableName -> tableName
            BucketName -> bucketName
            FunctionName -> functionName
            ResourceId -> resourceId
        """
        if not placeholder:
            return placeholder
        return placeholder[0].lower() + placeholder[1:]

    def _generate_fake_value(self, placeholder: str, service_prefix: str) -> str:
        """Generate a realistic fake value for an ARN placeholder."""
        suffix = f"{self._rng.randint(1000, 9999):04d}"
        lower = placeholder.lower()

        # Specific patterns
        if lower == "bucketname":
            return f"my-bucket-{suffix}"
        if lower == "key" or lower == "objectkey":
            return f"data/file-{suffix}.json"
        if lower in ("tablename", "table"):
            return f"my-table-{suffix}"
        if lower == "functionname":
            return f"my-function-{suffix}"
        if lower == "rolename":
            return f"my-role-{suffix}"
        if lower == "username":
            return f"user-{suffix}"
        if lower == "policyname":
            return f"my-policy-{suffix}"
        if lower == "clustername" or lower == "clusterid":
            return f"my-cluster-{suffix}"
        if lower == "dbinstanceidentifier":
            return f"my-db-{suffix}"
        if lower == "queuename":
            return f"my-queue-{suffix}"
        if lower == "topicname":
            return f"my-topic-{suffix}"
        if lower == "streamname":
            return f"my-stream-{suffix}"
        if lower == "keyid":
            return str(uuid.UUID(int=self._rng.getrandbits(128), version=4))
        if lower == "stackname":
            return f"my-stack-{suffix}"
        if lower == "secretid":
            return f"my-secret-{suffix}"
        if lower == "loggroup" or lower == "loggroupname":
            return f"/aws/{service_prefix}/my-log-group-{suffix}"
        if lower == "domainname":
            return f"my-domain-{suffix}"
        if lower == "repositoryname":
            return f"my-repo-{suffix}"
        if lower == "distributionid":
            return f"E{self._rng.randint(10000000, 99999999)}"
        if lower == "hostedzoneid":
            return f"Z{self._rng.randint(10000000, 99999999)}"
        if lower == "vaultname":
            return f"my-vault-{suffix}"
        if lower == "certificateid":
            return str(uuid.UUID(int=self._rng.getrandbits(128), version=4))
        if lower == "apiid":
            return f"api-{suffix}"
        if lower == "detectorid":
            return f"d{self._rng.randbytes(16).hex()[:20]}"
        if lower == "analyzerid" or lower == "analyzername":
            return f"my-analyzer-{suffix}"

        # Generic patterns based on suffix
        if lower.endswith("id"):
            return f"{self._rng.randbytes(8).hex()}"
        if lower.endswith("name"):
            return f"my-{service_prefix}-resource-{suffix}"
        if lower.endswith("arn"):
            return f"arn:aws:{service_prefix}:us-east-1:123456789012:resource/{suffix}"

        return f"{placeholder.lower()}-{suffix}"

    def _random_principal_id(self, prefix: str) -> str:
        """Generate a random AWS principal ID."""
        chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        rand_part = "".join(self._rng.choices(chars, k=16))
        return f"{prefix}{rand_part}"

    def _build_user_identity(self, account: str) -> dict:
        """Build a random userIdentity block."""
        choice = self._rng.randint(0, 4)

        if choice <= 1:
            # IAMUser
            users = [
                "admin", "developer", "deployer",
                "security-auditor", "data-engineer",
            ]
            user = self._rng.choice(users)
            return {
                "type": "IAMUser",
                "principalId": self._random_principal_id("AIDA"),
                "arn": f"arn:aws:iam::{account}:user/{user}",
                "accountId": account,
                "userName": user,
            }
        else:
            # AssumedRole
            roles = [
                "DevOpsRole", "CICDPipeline", "AdminRole",
                "ReadOnlyRole", "LambdaExecutionRole",
            ]
            role = self._rng.choice(roles)
            session = f"session-{self._rng.randint(1000, 9999)}"
            pid = self._random_principal_id("AROA")
            return {
                "type": "AssumedRole",
                "principalId": f"{pid}:{session}",
                "arn": (
                    f"arn:aws:sts::{account}"
                    f":assumed-role/{role}/{session}"
                ),
                "accountId": account,
                "sessionContext": {
                    "attributes": {
                        "mfaAuthenticated": self._rng.choice(
                            ["true", "false"]
                        ),
                        "creationDate": "2025-01-15T09:00:00Z",
                    },
                    "sessionIssuer": {
                        "type": "Role",
                        "principalId": self._random_principal_id(
                            "AROA"
                        ),
                        "arn": (
                            f"arn:aws:iam::{account}"
                            f":role/{role}"
                        ),
                        "accountId": account,
                        "userName": role,
                    },
                },
            }

    def _assign_timestamps(self, events: list[dict]) -> None:
        """Assign chronological timestamps to events.

        Distributes events evenly across the last 30 days.
        """
        if not events:
            return

        now = datetime.now(tz=timezone.utc)
        start = now - timedelta(days=30)
        step = timedelta(days=30) / max(len(events), 1)

        for i, event in enumerate(events):
            ts = start + step * i
            event["eventTime"] = ts.strftime("%Y-%m-%dT%H:%M:%SZ")
