from bs4 import BeautifulSoup
from email import encoders as Encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from email.utils import formatdate
from logging import getLogger
from plone import api
from plone.base.utils import safe_bytes
from plone.base.utils import safe_text
from plone.memoize.view import memoize
from plone.rfc822.interfaces import IPrimaryFieldInfo
from Products.Five import BrowserView
from urllib.parse import quote

import markdown
import stoneagehtml


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 = ""

    @property
    @memoize
    def webhelpers(self):
        return api.content.get_view("webhelpers", self.context, self.request)

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

    @property
    @memoize
    def recipient(self):
        """You must 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."""
        raise NotImplementedError

    @property
    def sender(self):
        from_address = api.portal.get_registry_record("plone.email_from_address")
        from_name = api.portal.get_registry_record("plone.email_from_name")
        return formataddr((from_name, from_address))

    @property
    def reply_to(self):
        """You must override this to add a reply-to to header to the email."""
        pass

    @property
    def headers(self):
        """You do not normally need to provide the From, Subject and To headers
        here because they will be setup by the send_email method.

        Anyway this is not forbidden. If you want to CC or BCC people,
        add an header here.
        """
        headers = {"Date": formatdate(localtime=True)}
        # Some mail servers don't allow a comma in the recipient name even if
        # it's quoted.
        headers["To"] = self.recipient.replace(",", "")
        reply_to = self.reply_to
        if reply_to:
            headers["Reply-to"] = reply_to
        return headers

    @property
    def html_email(self):
        """It is normally html generated by the index method and stripped to
        work for every MUA.

        You can override the index method by registering a template in
        some zcml or with a plain python method.
        """
        html = markdown.markdown(self.index(), extensions=["nl2br"])
        return safe_bytes(stoneagehtml.compactify(html))

    @property
    def text_email(self):
        """By default transforms the html_email in to a text version."""
        # Produce an approximate textual rendering of the HTML string,
        # unless you have been given a better version as an argument
        soup = BeautifulSoup(self.html_email, features="lxml")
        # kill all script and style elements
        for script in soup(["script", "style"]):
            script.extract()

        anchors = []
        for cnt, it in enumerate(soup(["a"])):
            it.replace_with(f"{it.text}[{cnt + 1}]")
            anchors.append(it["href"])

        text = "{}\n\n{}".format(
            safe_text(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 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*=UTF-8''%s" % quote(field.filename),
        )
        return part

    @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.
        """
        # if we would like to include images in future, there should
        # probably be 'related' instead of 'mixed'
        msg = MIMEMultipart("mixed")
        for key, value in self.headers.items():
            msg[key] = value

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

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

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

    def send_email(self):
        body = self.body
        mh = api.portal.get_tool("MailHost")
        mh.send(body, mfrom=self.sender, subject=self.subject, charset="utf-8")

    def __call__(self):
        return self
