from django.conf import settings
from django.utils.encoding import force_str

from ..exceptions import AnymailRequestsAPIError
from ..message import AnymailRecipientStatus
from ..utils import get_anymail_setting, update_deep
from .base_requests import AnymailRequestsBackend, RequestsPayload


class EmailBackend(AnymailRequestsBackend):
    """
    SparkPost Email Backend
    """

    esp_name = "SparkPost"

    def __init__(self, **kwargs):
        """Init options from Django settings"""
        self.api_key = get_anymail_setting(
            "api_key", esp_name=self.esp_name, kwargs=kwargs, allow_bare=True
        )
        self.subaccount = get_anymail_setting(
            "subaccount", esp_name=self.esp_name, kwargs=kwargs, default=None
        )
        api_url = get_anymail_setting(
            "api_url",
            esp_name=self.esp_name,
            kwargs=kwargs,
            default="https://api.sparkpost.com/api/v1/",
        )
        if not api_url.endswith("/"):
            api_url += "/"
        super().__init__(api_url, **kwargs)

    def build_message_payload(self, message, defaults):
        return SparkPostPayload(message, defaults, self)

    def parse_recipient_status(self, response, payload, message):
        parsed_response = self.deserialize_json_response(response, payload, message)
        try:
            results = parsed_response["results"]
            accepted = results["total_accepted_recipients"]
            rejected = results["total_rejected_recipients"]
            transmission_id = results["id"]
        except (KeyError, TypeError) as err:
            raise AnymailRequestsAPIError(
                "Invalid SparkPost API response format",
                email_message=message,
                payload=payload,
                response=response,
                backend=self,
            ) from err

        # SparkPost doesn't (yet*) tell us *which* recipients were accepted or rejected.
        # (* looks like undocumented 'rcpt_to_errors' might provide this info.)
        # If all are one or the other, we can report a specific status;
        # else just report 'unknown' for all recipients.
        recipient_count = len(payload.recipients)
        if accepted == recipient_count and rejected == 0:
            status = "queued"
        elif rejected == recipient_count and accepted == 0:
            status = "rejected"
        else:  # mixed results, or wrong total
            status = "unknown"
        recipient_status = AnymailRecipientStatus(
            message_id=transmission_id, status=status
        )
        return {
            recipient.addr_spec: recipient_status for recipient in payload.recipients
        }


class SparkPostPayload(RequestsPayload):
    def __init__(self, message, defaults, backend, *args, **kwargs):
        http_headers = {
            "Authorization": backend.api_key,
            "Content-Type": "application/json",
        }
        if backend.subaccount is not None:
            http_headers["X-MSYS-SUBACCOUNT"] = backend.subaccount
        self.recipients = []  # all recipients, for backend parse_recipient_status
        self.cc_and_bcc = []  # for _finalize_recipients
        super().__init__(
            message, defaults, backend, headers=http_headers, *args, **kwargs
        )

    def get_api_endpoint(self):
        return "transmissions/"

    def serialize_data(self):
        self._finalize_recipients()
        self._check_content_options()
        return self.serialize_json(self.data)

    def _finalize_recipients(self):
        # https://www.sparkpost.com/docs/faq/cc-bcc-with-rest-api/
        # self.data["recipients"] is currently a list of all to-recipients. Must add all
        # cc and bcc recipients. Exactly how depends on whether this is a batch send.
        if self.is_batch():
            # For batch sends, must duplicate the cc/bcc for *every* to-recipient
            # (using each to-recipient's metadata and substitutions).
            extra_recipients = []
            for to_recipient in self.data["recipients"]:
                for email in self.cc_and_bcc:
                    extra = to_recipient.copy()  # gets "metadata" and "substitutions"
                    extra["address"] = {
                        "email": email.addr_spec,
                        "header_to": to_recipient["address"]["header_to"],
                    }
                    extra_recipients.append(extra)
            self.data["recipients"].extend(extra_recipients)
        else:
            # For non-batch sends, we need to patch up *everyone's* displayed
            # "To" header to show all the "To" recipients...
            full_to_header = ", ".join(
                to_recipient["address"]["header_to"]
                for to_recipient in self.data["recipients"]
            )
            for recipient in self.data["recipients"]:
                recipient["address"]["header_to"] = full_to_header
            # ... and then simply add the cc/bcc to the end of the list.
            # (There is no per-recipient data, or it would be a batch send.)
            self.data["recipients"].extend(
                {
                    "address": {
                        "email": email.addr_spec,
                        "header_to": full_to_header,
                    }
                }
                for email in self.cc_and_bcc
            )

    # SparkPost silently ignores certain "content" payload fields
    # when a template_id is used.
    IGNORED_WITH_TEMPLATE_ID = {
        # SparkPost API content.<field> -> feature name (for error message)
        "attachments": "attachments",
        "inline_images": "inline images",
        "headers": "extra headers and/or cc recipients",
        "from": "from_email",
        "reply_to": "reply_to",
    }

    def _check_content_options(self):
        if "template_id" in self.data["content"]:
            # subject, text, and html will cause 422 API Error:
            #     "message": "Both content object and template_id are specified",
            #     "code": "1301"
            # but others are silently ignored in a template send:
            ignored = [
                feature_name
                for field, feature_name in self.IGNORED_WITH_TEMPLATE_ID.items()
                if field in self.data["content"]
            ]
            if ignored:
                self.unsupported_feature("template_id with %s" % ", ".join(ignored))

    #
    # Payload construction
    #

    def init_payload(self):
        # The JSON payload:
        self.data = {
            "content": {},
            "recipients": [],
        }

    def set_from_email(self, email):
        if email:
            self.data["content"]["from"] = email.address

    def set_to(self, emails):
        if emails:
            # In the recipient address, "email" is the addr spec to deliver to,
            # and "header_to" is a fully-composed "To" header to display.
            # (We use "header_to" rather than "name" to simplify some logic
            # in _finalize_recipients; the results end up the same.)
            self.data["recipients"].extend(
                {
                    "address": {
                        "email": email.addr_spec,
                        "header_to": email.address,
                    }
                }
                for email in emails
            )
            self.recipients += emails

    def set_cc(self, emails):
        # https://www.sparkpost.com/docs/faq/cc-bcc-with-rest-api/
        if emails:
            # Add the Cc header, visible to all recipients:
            cc_header = ", ".join(email.address for email in emails)
            self.data["content"].setdefault("headers", {})["Cc"] = cc_header
            # Actual recipients are added later, in _finalize_recipients
            self.cc_and_bcc += emails
            self.recipients += emails

    def set_bcc(self, emails):
        if emails:
            # Actual recipients are added later, in _finalize_recipients
            self.cc_and_bcc += emails
            self.recipients += emails

    def set_subject(self, subject):
        self.data["content"]["subject"] = subject

    def set_reply_to(self, emails):
        if emails:
            self.data["content"]["reply_to"] = ", ".join(
                email.address for email in emails
            )

    def set_extra_headers(self, headers):
        if headers:
            self.data["content"].setdefault("headers", {}).update(headers)

    def set_text_body(self, body):
        self.data["content"]["text"] = body

    def set_html_body(self, body):
        if "html" in self.data["content"]:
            # second html body could show up through multiple alternatives,
            # or html body + alternative
            self.unsupported_feature("multiple html parts")
        self.data["content"]["html"] = body

    def add_alternative(self, content, mimetype):
        if mimetype.lower() == "text/x-amp-html":
            if "amp_html" in self.data["content"]:
                self.unsupported_feature("multiple html parts")
            self.data["content"]["amp_html"] = content
        else:
            super().add_alternative(content, mimetype)

    def set_attachments(self, atts):
        attachments = [
            {
                "name": att.name or "",
                "type": att.content_type,
                "data": att.b64content,
            }
            for att in atts
            if not att.inline
        ]
        if attachments:
            self.data["content"]["attachments"] = attachments

        inline_images = [
            {
                "name": att.cid,
                "type": att.mimetype,
                "data": att.b64content,
            }
            for att in atts
            if att.inline
        ]
        if inline_images:
            self.data["content"]["inline_images"] = inline_images

    # Anymail-specific payload construction
    def set_envelope_sender(self, email):
        self.data["return_path"] = email.addr_spec

    def set_metadata(self, metadata):
        self.data["metadata"] = metadata

    def set_merge_metadata(self, merge_metadata):
        for recipient in self.data["recipients"]:
            to_email = recipient["address"]["email"]
            if to_email in merge_metadata:
                recipient["metadata"] = merge_metadata[to_email]

    def set_merge_headers(self, merge_headers):
        def header_var(field):
            return "Header__" + field.title().replace("-", "_")

        merge_header_fields = set()

        for recipient in self.data["recipients"]:
            to_email = recipient["address"]["email"]
            if to_email in merge_headers:
                recipient_headers = merge_headers[to_email]
                recipient.setdefault("substitution_data", {}).update(
                    {header_var(key): value for key, value in recipient_headers.items()}
                )
                merge_header_fields.update(recipient_headers.keys())

        if merge_header_fields:
            headers = self.data.setdefault("content", {}).setdefault("headers", {})
            # Global substitution_data supplies defaults for defined headers:
            self.data.setdefault("substitution_data", {}).update(
                {
                    header_var(field): headers[field]
                    for field in merge_header_fields
                    if field in headers
                }
            )
            # Indirect merge_headers through substitution_data:
            headers.update(
                {field: "{{%s}}" % header_var(field) for field in merge_header_fields}
            )

    def set_send_at(self, send_at):
        try:
            start_time = send_at.replace(microsecond=0).isoformat()
        except (AttributeError, TypeError):
            start_time = send_at  # assume user already formatted
        self.data.setdefault("options", {})["start_time"] = start_time

    def set_tags(self, tags):
        if len(tags) > 0:
            self.data["campaign_id"] = tags[0]
            if len(tags) > 1:
                self.unsupported_feature("multiple tags (%r)" % tags)

    def set_track_clicks(self, track_clicks):
        self.data.setdefault("options", {})["click_tracking"] = track_clicks

    def set_track_opens(self, track_opens):
        self.data.setdefault("options", {})["open_tracking"] = track_opens

    def set_template_id(self, template_id):
        self.data["content"]["template_id"] = template_id
        # Must remove empty string "content" params when using stored template.
        # (Non-empty params are left in place, to cause API error.)
        for content_param in ["subject", "text", "html"]:
            try:
                if not self.data["content"][content_param]:
                    del self.data["content"][content_param]
            except KeyError:
                pass
        # "from" is also silently ignored. Strip it if empty or DEFAULT_FROM_EMAIL,
        # else leave in place to cause error in _check_content_options.
        try:
            from_email = self.data["content"]["from"]
            if not from_email or from_email == force_str(settings.DEFAULT_FROM_EMAIL):
                del self.data["content"]["from"]
        except KeyError:
            pass

    def set_merge_data(self, merge_data):
        for recipient in self.data["recipients"]:
            to_email = recipient["address"]["email"]
            if to_email in merge_data:
                recipient["substitution_data"] = merge_data[to_email]

    def set_merge_global_data(self, merge_global_data):
        self.data["substitution_data"] = merge_global_data

    # ESP-specific payload construction
    def set_esp_extra(self, extra):
        update_deep(self.data, extra)
