"""Django email backend that sends via the Cloudflare Email Sending REST API.

Django is not a Worker, so the Workers `send_email` binding is unavailable; this
uses the REST endpoint with a Bearer token instead. Implemented as an
EMAIL_BACKEND so every existing call site (django.core.mail.send_mail,
EmailMultiAlternatives, admin mail) keeps working untouched.

REST field names differ from the Workers binding in ways that fail as 400s:
`from` takes `address` (not `email`), and `reply_to` is snake_case (not
`replyTo`). See references/rest-api.md in the cloudflare-email-service skill.

Transactional only. Cloudflare's Email Sending terms exclude marketing/bulk
mail, and the per-message recipient cap (to + cc + bcc) is 50.
"""

import base64
import json
import logging
import ssl
import time
import urllib.error
import urllib.request

from email.utils import parseaddr

from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend

logger = logging.getLogger(__name__)

API_URL = (
    "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send"
)
# Only 429 and 500-class responses can succeed on retry; a 400 is a malformed
# message and a 401 is a bad token, neither of which change by trying again.
RETRY_STATUSES = frozenset({429, 500, 502, 503, 504})
MAX_RECIPIENTS = 50


def _ssl_context():
    """TLS context with an explicit CA bundle.

    urllib uses the OpenSSL default CA store, which is empty on hosts that never
    installed one (macOS with a python.org build, and slim Linux containers) —
    every request then fails with CERTIFICATE_VERIFY_FAILED. boto3 and requests
    avoid this by shipping certifi, so prefer that bundle and fall back to the
    system store when certifi is absent. Never disable verification: this request
    carries an API token.
    """
    try:
        import certifi
    except ImportError:
        return ssl.create_default_context()
    return ssl.create_default_context(cafile=certifi.where())


class CloudflareEmailBackend(BaseEmailBackend):
    """Send EmailMessage objects through Cloudflare Email Sending."""

    def __init__(self, fail_silently=False, **kwargs):
        super().__init__(fail_silently=fail_silently, **kwargs)
        self.account_id = getattr(settings, "CLOUDFLARE_ACCOUNT_ID", "")
        self.api_token = getattr(settings, "CLOUDFLARE_EMAIL_API_TOKEN", "")
        self.timeout = getattr(settings, "CLOUDFLARE_EMAIL_TIMEOUT", 10)
        self.max_retries = getattr(settings, "CLOUDFLARE_EMAIL_MAX_RETRIES", 2)
        self._ssl = _ssl_context()

    def send_messages(self, email_messages):
        if not email_messages:
            return 0
        if not (self.account_id and self.api_token):
            if not self.fail_silently:
                raise ValueError(
                    "CloudflareEmailBackend requires CLOUDFLARE_ACCOUNT_ID and "
                    "CLOUDFLARE_EMAIL_API_TOKEN."
                )
            logger.error("cloudflare_email_not_configured")
            return 0

        sent = 0
        for message in email_messages:
            try:
                if self._send(message):
                    sent += 1
            except Exception:
                logger.exception("cloudflare_email_send_failed")
                if not self.fail_silently:
                    raise
        return sent

    @staticmethod
    def _address(value):
        """Reduce "Name <a@b.com>" to the bare address the API expects.

        Django's sanitize_address is deprecated in Django 7; parseaddr is the
        stdlib equivalent for the only transform needed here.
        """
        return parseaddr(value)[1]

    def _payload(self, message):
        encoding = message.encoding or settings.DEFAULT_CHARSET
        to = [self._address(a) for a in message.to]
        cc = [self._address(a) for a in message.cc]
        bcc = [self._address(a) for a in message.bcc]
        if not to and not cc and not bcc:
            raise ValueError("Email has no recipients.")
        if len(to) + len(cc) + len(bcc) > MAX_RECIPIENTS:
            raise ValueError(
                f"Cloudflare Email Sending allows at most {MAX_RECIPIENTS} "
                "recipients per message (to + cc + bcc)."
            )

        payload = {
            "to": to,
            # A display name would need RFC-5322 quoting the API rejects
            # inconsistently; the bare address is unambiguous.
            "from": self._address(message.from_email),
            "subject": message.subject,
        }
        if cc:
            payload["cc"] = cc
        if bcc:
            payload["bcc"] = bcc
        if message.reply_to:
            payload["reply_to"] = self._address(message.reply_to[0])

        # Django puts the primary body in .body with content_subtype naming its
        # type, and any alternative (usually text/html) in .alternatives.
        if message.content_subtype == "html":
            payload["html"] = message.body
        else:
            payload["text"] = message.body
        for alt in getattr(message, "alternatives", None) or []:
            content, mimetype = alt[0], alt[1]
            if mimetype == "text/html":
                payload["html"] = content
            elif mimetype == "text/plain":
                payload["text"] = content
        if "html" not in payload and "text" not in payload:
            raise ValueError("Email has neither a text nor an HTML body.")

        attachments = []
        for attachment in message.attachments:
            # Django allows either a 3-tuple or a MIMEBase; only the tuple form
            # carries the filename and mimetype this API needs.
            if not isinstance(attachment, (tuple, list)) or len(attachment) != 3:
                raise ValueError(
                    "CloudflareEmailBackend supports only (filename, content, "
                    "mimetype) attachments."
                )
            filename, content, mimetype = attachment
            if isinstance(content, str):
                content = content.encode(encoding)
            attachments.append({
                "content": base64.b64encode(content).decode("ascii"),
                "filename": filename,
                "type": mimetype or "application/octet-stream",
                "disposition": "attachment",
            })
        if attachments:
            payload["attachments"] = attachments
        return payload

    def _send(self, message):
        payload = self._payload(message)
        url = API_URL.format(account_id=self.account_id)
        body = json.dumps(payload).encode("utf-8")

        for attempt in range(self.max_retries + 1):
            request = urllib.request.Request(
                url,
                data=body,
                method="POST",
                headers={
                    "Authorization": f"Bearer {self.api_token}",
                    "Content-Type": "application/json",
                },
            )
            try:
                with urllib.request.urlopen(
                    request, timeout=self.timeout, context=self._ssl
                ) as response:
                    result = json.loads(response.read() or b"{}")
                bounced = (result.get("result") or {}).get("permanent_bounces") or []
                if bounced:
                    # Accepted by the API but undeliverable: surface it, since a
                    # silent bounce looks identical to success at the call site.
                    logger.warning("cloudflare_email_bounced recipients=%s", bounced)
                return True
            except urllib.error.HTTPError as exc:
                detail = exc.read().decode("utf-8", "replace")[:500]
                retryable = exc.code in RETRY_STATUSES
                if retryable and attempt < self.max_retries:
                    time.sleep(2 ** attempt)
                    continue
                logger.error(
                    "cloudflare_email_http_error status=%s detail=%s", exc.code, detail
                )
                if not self.fail_silently:
                    raise
                return False
            except (urllib.error.URLError, TimeoutError) as exc:
                if attempt < self.max_retries:
                    time.sleep(2 ** attempt)
                    continue
                logger.error("cloudflare_email_network_error error=%s", exc)
                if not self.fail_silently:
                    raise
                return False
        return False
