import boto3
import re
import os
import yaml
from typing import Optional, Dict, Any, Union, TYPE_CHECKING
from pydantic import BaseModel, validator, Field, model_validator
from .models.base import InfraConfig
from .features.infra.infra_config import InfraConfig as InfraConfigManager

if TYPE_CHECKING:
    from mypy_boto3_sns import SNSClient, SNSServiceResource
    from boto3.resources.base import ServiceResource

class SNSTopicInfraConfig(InfraConfig):
    """
    A construct for creating and interacting with an AWS SNS Topic.

    The iic analyzer will detect the instantiation of this class and
    generate an AWS::SNS::Topic resource in the CloudFormation template
    with the specified configuration.
    
    The name parameter is used to derive both the logical_id and topic_name.
    For FIFO topics, the name should end with '.fifo' and will be used as-is.
    For standard topics, the name will be used as-is.
    
    Examples:
        SNSTopicInfraConfig(name="my-topic")  # Creates logical_id="MyTopic", topic_name="my-topic"
        SNSTopicInfraConfig(name="orders.fifo", fifo_topic=True)  # Creates logical_id="Orders", topic_name="orders.fifo"
    """

    name: str = Field(..., description="Name of the SNS topic (used to derive logical_id and topic_name)")
    logical_id: Optional[str] = Field(None, description="Override CloudFormation Logical ID (auto-generated from name if not provided)")
    topic_name: Optional[str] = Field(None, description="Override SNS topic name (auto-generated from name if not provided)")
    fifo_topic: bool = Field(False, description="Whether this is a FIFO topic")
    content_based_deduplication: bool = Field(False, description="Enable content-based deduplication")
    display_name: Optional[str] = Field(None, description="Display name for the topic")
    kms_master_key_id: Optional[str] = Field(None, description="KMS key ID for encryption")
    tags: Dict[str, str] = Field(default_factory=dict, description="Tags for the topic")

    @validator('name')
    def validate_name(cls, v):
        """Validate name parameter."""
        if not v or not v.strip():
            raise ValueError("Name cannot be empty")
        return v.strip()

    @validator('logical_id')
    def validate_logical_id(cls, v):
        """Validate CloudFormation Logical ID format."""
        if v is not None and not re.match(r'^[a-zA-Z][a-zA-Z0-9]*$', v):
            raise ValueError("Logical ID must start with a letter and contain only alphanumeric characters")
        return v

    @model_validator(mode='after')
    def derive_and_validate_fields(self):
        """Derive logical_id and topic_name from name, and validate all fields."""
        # Derive logical_id from name if not provided
        if self.logical_id is None:
            # Convert name to PascalCase for logical_id
            # Remove .fifo suffix if present, convert to PascalCase
            base_name = self.name.replace('.fifo', '')
            self.logical_id = self._derive_logical_id(base_name)
        
        # Derive topic_name from name if not provided
        if self.topic_name is None:
            self.topic_name = self.name
        
        # Validate FIFO settings
        if self.fifo_topic:
            if not self.topic_name.endswith('.fifo'):
                raise ValueError("FIFO topic names must end with '.fifo'")
        else:
            if self.topic_name.endswith('.fifo'):
                raise ValueError("Standard topic names cannot end with '.fifo'")
        
        # Validate content-based deduplication
        if self.content_based_deduplication and not self.fifo_topic:
            raise ValueError("Content-based deduplication can only be enabled for FIFO topics")
        
        return self

    def _derive_logical_id(self, name: str) -> str:
        """
        Derive logical ID from name, handling various naming conventions.
        
        Supports:
        - kebab-case: my-topic -> MyTopic
        - snake_case: my_topic -> MyTopic  
        - space-separated: my topic -> MyTopic
        - camelCase: myTopic -> MyTopic
        - PascalCase: MyTopic -> MyTopic
        - UPPER_CASE: MY_TOPIC -> MyTopic
        """
        # First split on separators (hyphens, underscores, spaces)
        parts = re.split(r'[-_\s]+', name)
        
        # If we only got one part, check if it's camelCase/PascalCase
        if len(parts) == 1 and parts[0]:
            # Split on case boundaries for camelCase/PascalCase
            # This regex finds positions where a lowercase letter is followed by uppercase
            camel_parts = re.split(r'(?<=[a-z])(?=[A-Z])', parts[0])
            if len(camel_parts) > 1:
                parts = camel_parts
        
        # Convert each part to proper case (first letter uppercase, rest lowercase)
        logical_id_parts = []
        for part in parts:
            if part:  # Skip empty parts
                logical_id_parts.append(part.capitalize())
        
        return ''.join(logical_id_parts)

    @validator('display_name')
    def validate_display_name(cls, v):
        """Validate display name length."""
        if v is not None and len(v) > 100:
            raise ValueError("Display name cannot exceed 100 characters")
        return v

    @validator('tags')
    def validate_tags(cls, v):
        """Ensure tags are in the correct format for CloudFormation conversion."""
        if v is not None:
            for key, value in v.items():
                if not isinstance(key, str) or not isinstance(value, str):
                    raise ValueError("Tags must be string key-value pairs")
        return v

    def to_cloudformation_properties(self) -> Dict[str, Any]:
        """
        Convert this SNSTopicInfraConfig to CloudFormation Topic properties.

        Returns:
            Dictionary of CloudFormation properties
        """
        properties = {}

        if self.topic_name:
            properties["TopicName"] = self.topic_name

        if self.fifo_topic:
            properties["FifoTopic"] = True

        if self.content_based_deduplication:
            properties["ContentBasedDeduplication"] = True

        if self.display_name:
            properties["DisplayName"] = self.display_name

        if self.kms_master_key_id:
            properties["KmsMasterKeyId"] = self.kms_master_key_id

        if self.tags:
            properties["Tags"] = [{"Key": k, "Value": v} for k, v in self.tags.items()]

        return properties

    def _resolve_topic_arn(self, session: boto3.Session) -> str:
        """
        Resolves topic ARN from infra.yml and validates it exists in AWS.
        """
        infra_config = InfraConfigManager.load()
        # Construct the output key from the resource name
        output_key = f"{self.name}TopicArn"
        topic_arn = infra_config.exports[output_key]
        
        # Verify the topic exists in AWS
        sns_client = session.client('sns')
        try:
            sns_client.get_topic_attributes(TopicArn=topic_arn)
            return topic_arn
        except sns_client.exceptions.NotFoundException:
            raise ValueError(f"Topic ARN '{topic_arn}' (from infra.yml) does not exist in AWS.")
    
    def get(self, session: Optional[boto3.Session] = None) -> 'SNSServiceResource':
        """
        Returns the boto3 SNS topic resource for this topic configuration.
        
        Args:
            session: Optional boto3 session. If not provided, creates a new one.
            
        Returns:
            boto3 SNS topic resource object
        """
        session = session or boto3.Session()
        topic_arn = self._resolve_topic_arn(session)
        return session.resource('sns').Topic(topic_arn)
    
    def get_client(self, session: Optional[boto3.Session] = None) -> 'SNSClient':
        """
        Returns the boto3 SNS client.
        
        Args:
            session: Optional boto3 session. If not provided, creates a new one.
            
        Returns:
            boto3 SNS client object
        """
        session = session or boto3.Session()
        return session.client('sns')


class FilterPolicyScope:
    """Enum for filter policy scope."""
    MESSAGE_ATTRIBUTES = "MessageAttributes"
    MESSAGE_BODY = "MessageBody"


class FilterPolicyBuilder:
    """
    Builder for creating SNS filter policies.

    This class provides a type-safe way to build filter policies for SNS subscriptions
    instead of requiring users to write raw JSON.
    """

    def __init__(self):
        self._policy = {}

    def with_attribute(self, key: str, values: list) -> 'FilterPolicyBuilder':
        """
        Add a filter for message attributes.

        Args:
            key: Attribute name
            values: List of allowed values

        Returns:
            FilterPolicyBuilder instance for method chaining
        """
        self._policy[key] = values
        return self

    def with_numeric_attribute(self, key: str, condition: str, value: Union[int, float]) -> 'FilterPolicyBuilder':
        """
        Add a numeric filter for message attributes.

        Args:
            key: Attribute name
            condition: Numeric condition (=, >, <, >=, <=)
            value: Numeric value

        Returns:
            FilterPolicyBuilder instance for method chaining
        """
        if condition not in ['=', '>', '<', '>=', '<=', '']:
            raise ValueError(f"Invalid numeric condition: {condition}")

        self._policy[key] = {condition: value}
        return self

    def with_prefix_attribute(self, key: str, prefix: str) -> 'FilterPolicyBuilder':
        """
        Add a prefix filter for message attributes.

        Args:
            key: Attribute name
            prefix: Prefix to match

        Returns:
            FilterPolicyBuilder instance for method chaining
        """
        self._policy[key] = {"prefix": prefix}
        return self

    def with_suffix_attribute(self, key: str, suffix: str) -> 'FilterPolicyBuilder':
        """
        Add a suffix filter for message attributes.

        Args:
            key: Attribute name
            suffix: Suffix to match

        Returns:
            FilterPolicyBuilder instance for method chaining
        """
        self._policy[key] = {"suffix": suffix}
        return self

    def with_anything_but_attribute(self, key: str, values: list) -> 'FilterPolicyBuilder':
        """
        Add a filter that matches anything except the specified values.

        Args:
            key: Attribute name
            values: List of values to exclude

        Returns:
            FilterPolicyBuilder instance for method chaining
        """
        self._policy[key] = {"anything-but": values}
        return self

    def build(self) -> Dict[str, Any]:
        """
        Build the filter policy dictionary.

        Returns:
            Filter policy as a dictionary
        """
        return self._policy.copy()

    def is_empty(self) -> bool:
        """
        Check if the filter policy is empty.

        Returns:
            True if no filters have been added
        """
        return len(self._policy) == 0


class SNSSubscription(InfraConfig):
    """
    A construct for creating and interacting with an AWS SNS Subscription.

    The iic analyzer will detect the instantiation of this class and
    generate an AWS::SNS::Subscription resource in the CloudFormation template
    with the specified configuration.
    
    The name parameter is used to derive the logical_id. You can override it if needed.
    
    Examples:
        SNSSubscription(name="email-sub", topic=my_topic, protocol="email", endpoint="user@example.com")
        SNSSubscription(name="webhook-sub", topic="my-topic", protocol="https", endpoint="https://api.example.com/webhook")
    """

    name: str = Field(..., description="Name of the subscription (used to derive logical_id)")
    logical_id: Optional[str] = Field(None, description="Override CloudFormation Logical ID (auto-generated from name if not provided)")
    topic: Union[str, SNSTopicInfraConfig] = Field(..., description="Topic ARN, SNSTopicInfraConfig object, or variable reference")
    protocol: str = Field(..., description="Subscription protocol")
    endpoint: str = Field(..., description="Subscription endpoint")
    raw_message_delivery: bool = Field(False, description="Enable raw message delivery")
    filter_policy: Optional[Dict[str, Any]] = Field(None, description="Filter policy")
    filter_policy_scope: str = Field("MessageAttributes", description="Filter policy scope")
    subscription_role_arn: Optional[str] = Field(None, description="IAM role ARN")

    @validator('name')
    def validate_name(cls, v):
        """Validate name parameter."""
        if not v or not v.strip():
            raise ValueError("Name cannot be empty")
        return v.strip()

    @validator('logical_id')
    def validate_logical_id(cls, v):
        """Validate CloudFormation Logical ID format."""
        if v is not None and not re.match(r'^[a-zA-Z][a-zA-Z0-9]*$', v):
            raise ValueError("Logical ID must start with a letter and contain only alphanumeric characters")
        return v

    @model_validator(mode='after')
    def derive_logical_id(self):
        """Derive logical_id from name if not provided."""
        if self.logical_id is None:
            self.logical_id = self._derive_logical_id(self.name)
        return self

    def _derive_logical_id(self, name: str) -> str:
        """
        Derive logical ID from name, handling various naming conventions.
        
        Supports:
        - kebab-case: my-topic -> MyTopic
        - snake_case: my_topic -> MyTopic  
        - space-separated: my topic -> MyTopic
        - camelCase: myTopic -> MyTopic
        - PascalCase: MyTopic -> MyTopic
        - UPPER_CASE: MY_TOPIC -> MyTopic
        """
        # First split on separators (hyphens, underscores, spaces)
        parts = re.split(r'[-_\s]+', name)
        
        # If we only got one part, check if it's camelCase/PascalCase
        if len(parts) == 1 and parts[0]:
            # Split on case boundaries for camelCase/PascalCase
            # This regex finds positions where a lowercase letter is followed by uppercase
            camel_parts = re.split(r'(?<=[a-z])(?=[A-Z])', parts[0])
            if len(camel_parts) > 1:
                parts = camel_parts
        
        # Convert each part to proper case (first letter uppercase, rest lowercase)
        logical_id_parts = []
        for part in parts:
            if part:  # Skip empty parts
                logical_id_parts.append(part.capitalize())
        
        return ''.join(logical_id_parts)

    @validator('protocol')
    def validate_protocol(cls, v):
        """Validate protocol is supported."""
        valid_protocols = ['http', 'https', 'email', 'email-json', 'sms', 'sqs', 'lambda', 'firehose']
        if v not in valid_protocols:
            raise ValueError(f"Protocol must be one of: {', '.join(valid_protocols)}")
        return v

    @validator('endpoint')
    def validate_endpoint(cls, v, values):
        """Validate endpoint based on protocol."""
        protocol = values.get('protocol')
        if not protocol:
            return v

        if protocol in ['http', 'https']:
            if not (v.startswith('http://') or v.startswith('https://')):
                raise ValueError("HTTP/HTTPS endpoints must be valid URLs starting with http:// or https://")
        elif protocol in ['email', 'email-json']:
            if not re.match(r'^[^@]+@[^@]+\.[^@]+$', v):
                raise ValueError("Email endpoints must be valid email addresses")
        elif protocol == 'sms':
            if not re.match(r'^\+[1-9]\d{1,14}$', v):
                raise ValueError("SMS endpoints must be in E.164 format (+ followed by digits)")
        elif protocol in ['sqs', 'lambda', 'firehose']:
            # These should be ARNs - basic validation
            if not v.startswith('arn:aws:'):
                raise ValueError(f"{protocol.upper()} endpoints must be valid ARNs")

        return v

    @validator('raw_message_delivery')
    def validate_raw_message_delivery(cls, v, values):
        """Validate raw message delivery is only used with supported protocols."""
        protocol = values.get('protocol')
        if v and protocol not in ['sqs', 'http', 'https', 'firehose']:
            raise ValueError("Raw message delivery is only supported for SQS, HTTP, HTTPS, and Firehose protocols")
        return v

    @validator('filter_policy_scope')
    def validate_filter_policy_scope(cls, v):
        """Validate filter policy scope."""
        if v not in [FilterPolicyScope.MESSAGE_ATTRIBUTES, FilterPolicyScope.MESSAGE_BODY]:
            raise ValueError("Filter policy scope must be MessageAttributes or MessageBody")
        return v


    @validator('subscription_role_arn')
    def validate_subscription_role_arn(cls, v):
        """Validate subscription role ARN format."""
        if v and not v.startswith('arn:aws:iam::'):
            raise ValueError("Subscription role ARN must be a valid IAM role ARN")
        return v

    @model_validator(mode='after')
    def validate_protocol_with_topic(self):
        """Validate protocol compatibility with topic type."""
        # Check if topic is a FIFO topic
        if isinstance(self.topic, SNSTopicInfraConfig) and self.topic.fifo_topic:
            # FIFO topics only support SQS protocol
            if self.protocol != 'sqs':
                raise ValueError(f"FIFO topics only support 'sqs' protocol, got '{self.protocol}'. "
                               f"Use a standard topic for {self.protocol} subscriptions.")
        return self

    def to_cloudformation_properties(self) -> Dict[str, Any]:
        """
        Convert this SNSSubscription to CloudFormation Subscription properties.

        Returns:
            Dictionary of CloudFormation properties
        """
        properties = {
            "Protocol": self.protocol,
            "Endpoint": self.endpoint
        }

        # Handle topic reference
        if isinstance(self.topic, SNSTopicInfraConfig):
            # Use the logical_id + "Topic" format to match CloudFormation resource naming
            properties["TopicArn"] = {"Ref": f"{self.topic.logical_id}Topic"}
        elif isinstance(self.topic, str) and self.topic.startswith('__VARIABLE__:'):
            # This is a variable reference to a topic, keep it as-is for now
            # It will be resolved during CloudFormation generation
            properties["TopicArn"] = self.topic
        else:
            properties["TopicArn"] = self.topic

        if self.raw_message_delivery:
            properties["RawMessageDelivery"] = True

        if self.filter_policy:
            properties["FilterPolicy"] = self.filter_policy

        if self.filter_policy_scope != "MessageAttributes":
            properties["FilterPolicyScope"] = self.filter_policy_scope

        if self.subscription_role_arn:
            properties["SubscriptionRoleArn"] = self.subscription_role_arn

        return properties
