import logging
from time import sleep
from datetime import datetime, timedelta

import boto3
from django.core.cache import cache
from django.core.mail.backends.base import BaseEmailBackend
from django.core.exceptions import ImproperlyConfigured
from requests.exceptions import RequestException as ResponseError

from . import settings
from . import signals
from . import utils

logger = settings.logger


def dkim_sign(message, dkim_domain=None, dkim_key=None, dkim_selector=None, dkim_headers=None):
    """Sign an email message with DKIM if the package and settings are available.

    Args:
        message (str): The email message as a string.
        dkim_domain (str): DKIM domain for signing.
        dkim_key (str): DKIM private key.
        dkim_selector (str): DKIM selector.
        dkim_headers (tuple): Headers to include in DKIM signing.

    Returns:
        str: The signed message or original message if signing fails.
    """
    try:
        import dkim
    except ImportError:
        logger.warning("DKIM package not installed, skipping signing")
        return message

    if not (dkim_domain and dkim_key):
        logger.debug("DKIM domain or key missing, skipping signing")
        return message

    try:
        sig = dkim.sign(
            message,
            dkim_selector,
            dkim_domain,
            dkim_key,
            include_headers=dkim_headers
        )
        return sig + message
    except Exception as e:
        logger.error(f"DKIM signing failed: {e}")
        return message


class SESBackend(BaseEmailBackend):
    """Django email backend for Amazon SES.

    Sends emails using AWS SES with support for DKIM signing and rate limiting.
    """

    def __init__(self, fail_silently=False, aws_access_key=None, aws_secret_key=None,
                 aws_region_name=None, aws_region_endpoint=None, aws_auto_throttle=None,
                 dkim_domain=None, dkim_key=None, dkim_selector=None, dkim_headers=None, **kwargs):
        """Initialize SES backend with AWS credentials and settings.

        Args:
            fail_silently (bool): If True, silently ignore errors.
            aws_access_key (str): AWS access key ID.
            aws_secret_key (str): AWS secret access key.
            aws_region_name (str): AWS region name.
            aws_region_endpoint (str): AWS SES endpoint URL.
            aws_auto_throttle (float): Throttling factor for SES rate limits.
            dkim_domain (str): DKIM domain for signing.
            dkim_key (str): DKIM private key.
            dkim_selector (str): DKIM selector.
            dkim_headers (tuple): Headers to include in DKIM signing.

        Raises:
            ImproperlyConfigured: If AWS credentials are missing.
        """
        super().__init__(fail_silently=fail_silently, **kwargs)
        self._access_key_id = aws_access_key or settings.ACCESS_KEY
        self._access_key = aws_secret_key or settings.SECRET_KEY
        self._region_name = aws_region_name or settings.AWS_SES_REGION_NAME
        self._endpoint_url = aws_region_endpoint or settings.AWS_SES_REGION_ENDPOINT
        self._throttle = aws_auto_throttle or settings.AWS_SES_AUTO_THROTTLE
        self.dkim_domain = dkim_domain or settings.DKIM_DOMAIN
        self.dkim_key = dkim_key or settings.DKIM_PRIVATE_KEY
        self.dkim_selector = dkim_selector or settings.DKIM_SELECTOR
        self.dkim_headers = dkim_headers or settings.DKIM_HEADERS

        self.connection = None

    def open(self):
        """Create a connection to the AWS SES API server.

        Returns:
            bool: True if a new connection was created, False otherwise.
        """
        if self.connection:
            return False

        try:
            # Build client kwargs conditionally
            client_kwargs = {
                'service_name': 'ses',
                'region_name': self._region_name,
                'endpoint_url': self._endpoint_url,
            }
            
            # Only add credentials if provided
            if self._access_key_id and self._access_key:
                client_kwargs.update({
                    'aws_access_key_id': self._access_key_id,
                    'aws_secret_access_key': self._access_key,
                })

            self.connection = boto3.client(**client_kwargs)
            return True
        except Exception as e:
            logger.error(f"Failed to connect to SES: {e}")
            if not self.fail_silently:
                raise
            return False

    def close(self):
        """Close the SES API connection."""
        self.connection = None

    def get_rate_limit(self):
        """Retrieve and cache the SES maximum send rate.

        Returns:
            float: The maximum send rate per second.

        Raises:
            Exception: If no connection is available to fetch the rate limit.
        """
        cache_key = f"ses_rate_limit_{self._access_key_id}"
        rate_limit = cache.get(cache_key)
        if rate_limit is not None:
            logger.debug(f"Retrieved cached rate limit: {rate_limit}")
            return rate_limit

        logger.debug("Fetching new rate limit from AWS SES")
        new_conn_created = self.open()
        if not self.connection:
            raise Exception("No connection to check SES rate limit.")

        try:
            quota_dict = self.connection.get_send_quota()
            rate_limit = float(quota_dict['MaxSendRate'])
            cache.set(cache_key, rate_limit, timeout=3600)  # Cache for 1 hour
            return rate_limit
        finally:
            if new_conn_created:
                self.close()

    def send_messages(self, email_messages):
        """Send one or more EmailMessage objects.

        Args:
            email_messages (list): List of EmailMessage objects to send.

        Returns:
            tuple: (number of messages sent, dictionary with sent/not sent info)
        """
        if not email_messages:
            logger.debug("No email messages to send")
            return 0, {"Sent": ""}

        new_conn_created = self.open()
        if not self.connection:
            logger.error("Failed to establish SES connection")
            return 0, {"Sent": ""}

        num_sent, sent_message, list_of_response = 0, {"Sent": ""}, []
        source = settings.AWS_SES_RETURN_PATH or settings.DEFAULT_FROM_EMAIL
        not_sent_list = []

        for message in email_messages:
            message.aws_ses_response = {'error': 'not sent yet'}
            signals.email_pre_send.send_robust(self.__class__, message=message)

            pre_filter_recipients = message.recipients()
            message.to = utils.filter_recipients(message.to)
            message.cc = utils.filter_recipients(message.cc)
            message.bcc = utils.filter_recipients(message.bcc)

            if not message.recipients():
                logger.debug("No recipients after filtering")
                message.aws_ses_response = {'error': 'no recipients left after filtering'}
                list_of_response.append({'error': 'no recipients left after filtering'})
                continue

            not_sent_list.extend([email for email in pre_filter_recipients if email not in message.recipients()])

            if settings.AWS_SES_CONFIGURATION_SET and 'X-SES-CONFIGURATION-SET' not in message.extra_headers:
                if callable(settings.AWS_SES_CONFIGURATION_SET):
                    message.extra_headers['X-SES-CONFIGURATION-SET'] = settings.AWS_SES_CONFIGURATION_SET(
                        message, dkim_domain=self.dkim_domain, dkim_key=self.dkim_key,
                        dkim_selector=self.dkim_selector, dkim_headers=self.dkim_headers
                    )
                else:
                    message.extra_headers['X-SES-CONFIGURATION-SET'] = settings.AWS_SES_CONFIGURATION_SET

            if self._throttle:
                now = datetime.now()
                cache_key = "ses_recent_send_times"
                recent_send_times = cache.get(cache_key, [])
                window = 2.0
                window_start = now - timedelta(seconds=window)
                recent_send_times = [t for t in recent_send_times if t > window_start]

                rate_limit = self.get_rate_limit()
                if len(recent_send_times) > rate_limit * window * self._throttle:
                    delta = now - recent_send_times[0]
                    total_seconds = delta.total_seconds()
                    delay = window - total_seconds
                    if delay > 0:
                        sleep(delay)

                recent_send_times.append(now)
                cache.set(cache_key, recent_send_times, timeout=2)

            try:
                response = self.connection.send_raw_email(
                    Source=source or message.from_email,
                    Destinations=message.recipients(),
                    RawMessage={'Data': dkim_sign(
                        message.message().as_string(),
                        dkim_key=self.dkim_key,
                        dkim_domain=self.dkim_domain,
                        dkim_selector=self.dkim_selector,
                        dkim_headers=self.dkim_headers
                    )}
                )
                message.aws_ses_response = response
                message.extra_headers.update({
                    'status': 200,
                    'message_id': response['MessageId'],
                    'request_id': response['ResponseMetadata']['RequestId']
                })
                num_sent += 1
                logger.info(
                    f"Sent email from {message.from_email} to {', '.join(message.recipients())}, "
                    f"message_id={message.extra_headers['message_id']}, request_id={message.extra_headers['request_id']}"
                )
                list_of_response.append(response)
                signals.email_post_send.send_robust(self.__class__, message=message)
            except ResponseError as err:
                logger.error(f"Failed to send email: {err}")
                message.extra_headers.update({
                    key: getattr(err, key, None) for key in ['status', 'reason', 'body', 'request_id', 'error_code', 'error_message']
                })
                list_of_response.append({'error': str(err)})
                if not self.fail_silently:
                    raise

        if not_sent_list:
            sent_message["Sent"] = ", ".join(not_sent_list)

        if new_conn_created:
            self.close()

        logger.debug(f"Sent {num_sent} messages, response: {list_of_response}")
        return num_sent, sent_message