from bs4 import BeautifulSoup
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
from logging import getLogger
from plone import api
from plone.base.utils import safe_bytes
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.rfc822.interfaces import IPrimaryFieldInfo
from Products.Five import BrowserView
from zope.i18n import translate

logger = getLogger(__name__)


class BaseEmail(BrowserView):
    """Helper view that renders an email, this is meant to be used as a
    base class.
    """

    # Use DumbWriters word wrapping to ensure that no text line
    # is longer than plain_text_maxcols characters.
    plain_text_maxcols = 72

    translatable_subject = ""

    _userprofile_active_states = ["enabled"]

    @property
    def target_language(self):
        """Return the language to use for the email

        By default the current language is used.
        """
        try:
            language = self.recipient_user.language
        except AttributeError:
            language = None
        return language or api.portal.get_current_language()

    def translate(self, message):
        """Translate an i18n message"""
        return translate(message, target_language=self.target_language)

    @property
    @memoize_contextless
    def portal(self):
        return api.portal.get()

    @property
    @memoize
    def subject(self):
        return self.translate(self.translatable_subject)

    @property
    @memoize
    def userprofiles(self):
        return api.content.get_view("view", self.portal.profiles, self.request)

    @property
    @memoize
    def recipient_user(self):
        """This is normally a ploneintranet.userprofile object"""
        return self.userprofiles.user

    @property
    @memoize
    def recipient(self):
        """You might override this to return any recipient you like
        or even None if you plan to provide a proper To, CC, BCC header
        using the headers method
        """
        if self.recipient_user is None:
            return None
        return self.recipient_user.display_name_email

    @property
    @memoize
    def sender_user(self):
        """None means the site admin configured in the mail control panel,
        but you can override this method to return a userprofile.
        To avoid being flagged as spam, it is generally better to set the
        reply_to_user instead.
        """
        return None

    @property
    @memoize
    def from_name(self):
        """If no sender_user if specified use the mail control panel value"""
        user = self.sender_user
        if not user:
            return api.portal.get_registry_record("plone.email_from_name")
        return self.userprofiles.get_fullname_for(user)

    @property
    @memoize
    def from_address(self):
        """If no sender_user if specified use the mail control panel value"""
        user = self.sender_user
        if not user:
            return api.portal.get_registry_record("plone.email_from_address")
        return user.email

    @property
    def sender(self):
        """If no sender_user if specified use the mail control panel value"""
        user = self.sender_user
        if not user:
            return f"{self.from_name} <{self.from_address}>"
        return user.display_name_email

    @property
    @memoize
    def reply_to_user(self):
        """Since #854 we generally don't want to set the sender, instead we
        should use the plone.email_from address, to avoid being flagged as spam.
        The Reply-To header should be set instead, where appropriate.
        """
        return None

    @property
    def reply_to(self):
        user = self.reply_to_user
        if user:
            return user.display_name_email

    @property
    def headers(self):
        """You do not normally need to provide the From, Subject and To
        headers here because they will be set up by the send_email method.
        Anyway this is not forbidden.
        Set the Reply-To header if self.reply_to is specified.
        If you want to CC or BCC people, add an header here.
        """
        headers = {"Date": formatdate(localtime=True)}
        reply_to = self.reply_to
        if reply_to:
            headers["Reply-To"] = Header(reply_to, "utf-8")
        return headers

    @property
    def html_email(self):
        """The generated HTML should be design-conformant and  work for every MUA.

        You can override the index method by
        registering a template in some zcml
        or with a plain python method.
        """
        return safe_bytes(self.index())

    def html_email2text_email(self, html_email):
        """Convert html to text

        Based on https://stackoverflow.com/a/24618186
        XXX does not respect the plain_text_maxcols attribute
        """
        soup = BeautifulSoup(html_email, features="lxml")
        # kill all script and style elements
        for script in soup(["script", "style"]):
            script.extract()

        anchors = []
        cnt = 0
        for it in soup(["a"]):
            if it.get_text().strip() in ("", "➜"):
                # ignore image and arrow hrefs in notifications
                continue
            it.replace_with(f"{it.text} [{cnt + 1}]")
            anchors.append(it["href"])
            cnt += 1

        text = "{}\n\n\n{}".format(
            soup.get_text(),
            "\n".join(f"[{cnt + 1}] {it}" for cnt, it in enumerate(anchors)),
        )

        # break into lines and remove leading and trailing space on each
        lines = (line.strip() for line in text.splitlines())
        # break multi-headlines into a line each
        chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
        # drop blank lines
        text = "\n".join(chunk for chunk in chunks if chunk)
        return text

    @property
    def text_email(self):
        """By default transforms the html_email in to a plain text version"""
        self.html_email2text_email(self.html_email)

    @property
    def attachments(self):
        """List here your attachments, they will be translated
        in to email parts by the attachment2part method.
        """
        return []

    def attachment2part(self, attachment):
        """Transform an attachment in to a part.
        Attachment is expected to be an IPrimaryFieldInfo adaptable object.
        If you want to attach something else you might want
        to override this method or provide some adapter
        """
        primary_field_info = IPrimaryFieldInfo(attachment)
        field = primary_field_info.value
        try:
            mimetype = field.contentType
        except Exception:
            logger.exception("Cannot get mimetype for %r", attachment)
            mimetype = "application/octet-stream"

        maintype, subtype = mimetype.partition("/")[::2]
        if maintype == "text":
            part = MIMEText(field.data, _subtype=subtype, _charset="UTF-8")
        else:
            part = MIMEBase(maintype, subtype)
            part.set_payload(field.data)
            encoders.encode_base64(part)
        part.add_header(
            "Content-Disposition", 'attachment; filename="%s"' % field.filename
        )
        return part

    def create_multipart_message(
        self, html_email=None, text_email=None, attachments=None, headers=None
    ):
        """Create a multipart message from the given parameters"""
        if html_email and text_email is None:
            text_email = self.html_email2text_email(html_email)
        if attachments is None:
            attachments = []
        if headers is None:
            headers = self.headers

        # if we would like to include images in future, there should
        # probably be 'related' instead of 'mixed'
        msg = MIMEMultipart("mixed")

        for key, value in headers.items():
            msg[key] = value

        msg.preamble = "This is a multi-part message in MIME format."

        alternatives = MIMEMultipart("alternative")
        msg.attach(alternatives)
        if text_email:
            alternatives.attach(MIMEText(text_email, "plain", _charset="UTF-8"))
        if html_email:
            alternatives.attach(MIMEText(html_email, "html", _charset="UTF-8"))

        # add the attachments
        for attachment in attachments:
            part = self.attachment2part(attachment)
            msg.attach(part)
        return msg

    @property
    def body(self):
        """Create a mime-message that will render HTML in popular
        MUAs, text in better ones.
        Taken with thanks from ftw.notification.email.
        """
        return self.create_multipart_message(
            html_email=self.html_email,
            text_email=self.text_email,
            attachments=self.attachments,
            headers=self.headers,
        )

    def send_email(self):
        recipient = self.recipient
        subject = self.subject

        if not recipient and not set(self.headers) & {"To", "Cc", "Bcc"}:
            logger.warning(
                "Missing recipient email. Cannot send mail with subject: %r.", subject
            )
            return

        try:
            api.portal.send_email(
                sender=self.sender,
                recipient=recipient,
                subject=subject,
                body=self.body,
                immediate=False,
            )
        except Exception:
            logger.exception(
                "Error sending email with subject: %r to %r.", subject, recipient
            )

    def __call__(self):
        return self
