# -*- coding: utf-8 -*-

#  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
#    Copyright (C) 2020 pwr
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import smtplib
import ssl
import threading
import socket
import mimetypes

from io import StringIO
from email.message import EmailMessage
from email.utils import formatdate, parseaddr, make_msgid
from email.generator import Generator
from flask_babel import lazy_gettext as N_

from cps.services.worker import CalibreTask
from cps.services import gmail
from cps.embed_helper import do_calibre_export
from cps import logger, config
from cps import gdriveutils
from cps.string_helper import strip_whitespaces


log = logger.create()

CHUNKSIZE = 8192


# Class for sending email with ability to get current progress
class EmailBase:

    transferSize = 0
    progress = 0

    def data(self, msg):
        self.transferSize = len(msg)
        (code, resp) = smtplib.SMTP.data(self, msg)
        self.progress = 0
        return (code, resp)

    def send(self, strg):
        """Send 'strg' to the server."""
        log.debug_no_auth('send: {}'.format(strg[:300]), stacklevel=2)
        if hasattr(self, 'sock') and self.sock:
            try:
                if self.transferSize:
                    lock = threading.Lock()
                    lock.acquire()
                    self.transferSize = len(strg)
                    lock.release()
                    for i in range(0, self.transferSize, CHUNKSIZE):
                        if isinstance(strg, bytes):
                            self.sock.send((strg[i:i + CHUNKSIZE]))
                        else:
                            self.sock.send((strg[i:i + CHUNKSIZE]).encode('utf-8'))
                        lock.acquire()
                        self.progress = i
                        lock.release()
                else:
                    self.sock.sendall(strg.encode('utf-8'))
            except socket.error:
                self.close()
                raise smtplib.SMTPServerDisconnected('Server not connected')
        else:
            raise smtplib.SMTPServerDisconnected('please run connect() first')

    @classmethod
    def _print_debug(cls, *args):
        log.debug(args)

    def getTransferStatus(self):
        if self.transferSize:
            lock2 = threading.Lock()
            lock2.acquire()
            value = int((float(self.progress) / float(self.transferSize))*100)
            lock2.release()
            return value / 100
        else:
            return 1


# Class for sending email with ability to get current progress, derived from emailbase class
class Email(EmailBase, smtplib.SMTP):

    def __init__(self, *args, **kwargs):
        smtplib.SMTP.__init__(self, *args, **kwargs)


# Class for sending ssl encrypted email with ability to get current progress, derived from emailbase class
class EmailSSL(EmailBase, smtplib.SMTP_SSL):

    def __init__(self, *args, **kwargs):
        smtplib.SMTP_SSL.__init__(self, *args, **kwargs)


class TaskEmail(CalibreTask):
    def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, id=0, internal=False):
        super(TaskEmail, self).__init__(task_message)
        self.subject = subject
        self.attachment = attachment
        self.settings = settings
        self.filepath = filepath
        self.recipient = recipient
        self.text = text
        self.asyncSMTP = None
        self.book_id = id
        self.results = dict()

    # from calibre code:
    # https://github.com/kovidgoyal/calibre/blob/731ccd92a99868de3e2738f65949f19768d9104c/src/calibre/utils/smtp.py#L60
    def get_msgid_domain(self):
        try:
            # Parse out the address from the From line, and then the domain from that
            from_email = parseaddr(self.settings["mail_from"])[1]
            msgid_domain = strip_whitespaces(from_email.partition('@')[2])
            # This can sometimes sneak through parseaddr if the input is malformed
            msgid_domain = strip_whitespaces(msgid_domain.rstrip('>'))
        except Exception:
            msgid_domain = ''
        return msgid_domain or 'calibre-web.com'

    def prepare_message(self):
        message = EmailMessage()
        # message = MIMEMultipart()
        message['From'] = self.settings["mail_from"]
        message['To'] = self.recipient
        message['Subject'] = self.subject
        message['Date'] = formatdate(localtime=True)
        message['Message-ID'] = make_msgid(domain=self.get_msgid_domain())
        message.set_content(self.text.encode('UTF-8'), "text", "plain")
        if self.attachment:
            data = self._get_attachment(self.filepath, self.attachment)
            if data:
                # Set mimetype
                content_type, encoding = mimetypes.guess_type(self.attachment)
                if content_type is None or encoding is not None:
                    content_type = 'application/octet-stream'
                main_type, sub_type = content_type.split('/', 1)
                message.add_attachment(data, maintype=main_type, subtype=sub_type, filename=self.attachment)
            else:
                self._handleError("Attachment not found")
                return
        return message

    def run(self, worker_thread):
        try:
            # create MIME message
            msg = self.prepare_message()
            if not msg:
                return
            if self.settings['mail_server_type'] == 0:
                self.send_standard_email(msg)
            else:
                self.send_gmail_email(msg)
        except MemoryError as e:
            log.error_or_exception(e, stacklevel=2)
            self._handleError('MemoryError sending e-mail: {}'.format(str(e)))
        except (smtplib.SMTPRecipientsRefused) as e:
            log.error_or_exception(e, stacklevel=2)
            self._handleError('Smtplib Error sending e-mail: {}'.format(
                (list(e.args[0].values())[0][1]).decode('utf-8)').replace("\n", '. ')))
        except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
            log.error_or_exception(e, stacklevel=2)
            if hasattr(e, "smtp_error"):
                text = e.smtp_error.decode('utf-8').replace("\n", '. ')
            elif hasattr(e, "message"):
                text = e.message
            elif hasattr(e, "args"):
                text = '\n'.join(e.args)
            else:
                text = ''
            self._handleError('Smtplib Error sending e-mail: {}'.format(text))
        except (socket.error) as e:
            log.error_or_exception(e, stacklevel=2)
            self._handleError('Socket Error sending e-mail: {}'.format(e.strerror))
        except Exception as ex:
            log.error_or_exception(ex, stacklevel=2)
            self._handleError('Error sending e-mail: {}'.format(ex))

    def send_standard_email(self, msg):
        use_ssl = int(self.settings.get('mail_use_ssl', 0))
        timeout = 600  # set timeout to 5mins

        # on python3 debugoutput is caught with overwritten _print_debug function
        log.debug("Start sending e-mail")
        if use_ssl == 2:
            context = ssl.create_default_context()
            self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
                                       timeout=timeout, context=context)
        else:
            self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)

        # link to logginglevel
        if logger.is_debug_enabled():
            self.asyncSMTP.set_debuglevel(1)
        if use_ssl == 1:
            context = ssl.create_default_context()
            self.asyncSMTP.starttls(context=context)
        if self.settings["mail_password_e"]:
            self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password_e"]))

        # Convert message to something to send
        fp = StringIO()
        gen = Generator(fp, mangle_from_=False)
        gen.flatten(msg)

        self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipient, fp.getvalue())
        self.asyncSMTP.quit()
        self._handleSuccess()
        log.debug("E-mail send successfully")

    def send_gmail_email(self, message):
        gmail.send_messsage(self.settings.get('mail_gmail_token', None), message)
        self._handleSuccess()

    @property
    def progress(self):
        if self.asyncSMTP is not None:
            return self.asyncSMTP.getTransferStatus()
        else:
            return self._progress

    @progress.setter
    def progress(self, x):
        """This gets explicitly set when handle(Success|Error) are called. In this case, remove the SMTP connection"""
        if x == 1:
            self.asyncSMTP = None
            self._progress = x

    def _get_attachment(self, book_path, filename):
        """Get file as MIMEBase message"""
        calibre_path = config.get_book_path()
        extension = os.path.splitext(filename)[1][1:]
        if config.config_use_google_drive:
            df = gdriveutils.getFileFromEbooksFolder(book_path, filename)
            if df:
                datafile = os.path.join(calibre_path, book_path, filename)
                if not os.path.exists(os.path.join(calibre_path, book_path)):
                    os.makedirs(os.path.join(calibre_path, book_path))
                df.GetContentFile(datafile)
            else:
                return None
            if config.config_binariesdir and config.config_embed_metadata:
                data_path, data_file = do_calibre_export(self.book_id, extension)
                datafile = os.path.join(data_path, data_file + "." + extension)
            with open(datafile, 'rb') as file_:
                data = file_.read()
            os.remove(datafile)
        else:
            datafile = os.path.join(calibre_path, book_path, filename)
            try:
                if config.config_binariesdir and config.config_embed_metadata:
                    data_path, data_file = do_calibre_export(self.book_id, extension)
                    datafile = os.path.join(data_path, data_file + "." + extension)
                with open(datafile, 'rb') as file_:
                    data = file_.read()
                if config.config_binariesdir and config.config_embed_metadata:
                    os.remove(datafile)
            except IOError as e:
                log.error_or_exception(e, stacklevel=2)
                log.error('The requested file could not be read. Maybe wrong permissions?')
                return None
        return data

    @property
    def name(self):
        return N_("E-mail")

    @property
    def is_cancellable(self):
        return False

    def __str__(self):
        return "E-mail {}, {}".format(self.name, self.subject)
