# By: Riasat Ullah
# This file contains functions to help us work with emails.

from botocore.exceptions import ClientError
from email.header import decode_header, make_header
from email.mime.application import MIMEApplication
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from taskcallrest import settings
from threading import Thread
from utils import constants, logging, s3, times, var_names
from validations import string_validator
import base64
import boto3
import configuration as configs
import os
import re
import smtplib
import time


class AmazonSesCredentials(object):
    '''
    This class gets the Amazon SES credentials for a given IAM user.
    '''
    def __init__(self, account_type=constants.notifier_email_account):
        self.account_type = account_type
        self.email, self.region, self.access_key, self.secret_key = self.get_aws_keys(self.account_type)

    @staticmethod
    def get_aws_keys(account_type):
        '''
        Gets the email credentials for the account that emails will be sent out from
        :param account_type: the type of the email account
        :return: (tuple) -> email address, password, server
        '''
        bucket = 'taskcall-prod-data'
        key = 'credentials/email_addr.json'
        try:
            data = s3.read_json(bucket, key)
            email = data[account_type][var_names.email]
            aws_region = data[account_type][var_names.aws_region]
            aws_access_key = data[account_type][var_names.aws_access_key_id]
            aws_secret_key = data[account_type][var_names.aws_secret_access_key]
            return email, aws_region, aws_access_key, aws_secret_key
        except KeyError as e:
            err = 'Unknown email account type - ' + account_type + '\n' + str(e)
            raise SystemError(err)
        except (OSError, IOError) as e:
            err = 'Could not read email address credentials file' + '\n' + str(e)
            raise OSError(err)


class AmazonSesDispatcher(Thread):
    '''
    This is an Amazon SES mail dispatcher thread class. It should be used when many
    multiple emails need to be sent simultaneously.
    '''

    def __init__(self, subject, message, to_addr, sender_credentials=None, with_logo=False, attachments=None,
                 delete_attachments=False, with_cc=None, with_bcc=None):
        self.subject = subject
        self.message = message
        self.to_addr = to_addr
        self.with_logo = with_logo
        self.attachments = attachments
        self.delete_attachments = delete_attachments
        self.cc_addr = with_cc
        self.bcc_addr = with_bcc

        if sender_credentials is None:
            self.sender_credentials = ses_credentials
        else:
            self.sender_credentials = sender_credentials
        Thread.__init__(self)

    def run(self):
        logging.info('Running amazon ses dispatcher at ' +
                     times.get_current_timestamp().strftime(constants.timestamp_format))
        attempts = 0
        try:
            # make multiple attempts to send the email
            attempts += 1
            amazon_ses_mailer(self.subject, self.message, self.to_addr, self.sender_credentials, self.with_logo,
                              attachments=self.attachments, delete_attachments=self.delete_attachments,
                              with_cc=self.cc_addr, with_bcc=self.bcc_addr)
        except Exception:
            if attempts <= 3:
                amazon_ses_mailer(self.subject, self.message, self.to_addr, self.sender_credentials, self.with_logo,
                                  attachments=self.attachments, delete_attachments=self.delete_attachments,
                                  with_cc=self.cc_addr, with_bcc=self.bcc_addr)
            else:
                raise


class AmazonSesBulkDispatcher(Thread):
    '''
    This is an Amazon SES bulk mail dispatcher thread class.
    It should be used when multiple emails need to be sent at once.
    :param messages: (list of dict) -> [ {email_subject: , email_body: , email_to: }, ...]
    '''

    def __init__(self, messages: list, sender_credentials=None, with_logo=False, attachments=None,
                 delete_attachments=False, with_cc=None, with_bcc=None):
        self.messages = messages
        self.with_logo = with_logo
        self.attachments = attachments
        self.delete_attachments = delete_attachments
        self.cc_addr = with_cc
        self.bcc_addr = with_bcc

        if sender_credentials is None:
            self.sender_credentials = ses_credentials
        else:
            self.sender_credentials = sender_credentials
        Thread.__init__(self)

    def run(self):
        logging.info('Running amazon ses bulk dispatcher at ' +
                     times.get_current_timestamp().strftime(constants.timestamp_format))
        try:
            client = boto3.client('ses', region_name=self.sender_credentials.region,
                                  aws_access_key_id=self.sender_credentials.access_key,
                                  aws_secret_access_key=self.sender_credentials.secret_key)
            for i in range(0, len(self.messages)):
                item = self.messages[i]

                if i > 0 and i % configs.mail_max_concurrency == 0:
                    logging.info('Waiting for 30 seconds to avoid hitting mail concurrency limit')
                    time.sleep(30)

                amazon_ses_mailer(item[var_names.email_subject], item[var_names.email_body], item[var_names.email_to],
                                  self.sender_credentials, self.with_logo, client, attachments=self.attachments,
                                  delete_attachments=self.delete_attachments, with_cc=self.cc_addr,
                                  with_bcc=self.bcc_addr)
        except Exception:
            raise


def amazon_ses_mailer(subject, message, to_addr, sender_credentials=None, with_logo=False, client=None,
                      attachments=None, delete_attachments=False, with_cc=None, with_bcc=None):
    '''
    Sends email message using Amazon SES from given an IAM user credentials.
    :param subject: subject of the email
    :param message: email body message
    :param to_addr: email address to send to
    :param sender_credentials: (AmazonSesCredentials) the credentials of the IAM user
    :param with_logo: True if TaskCall logo should be loaded and sent with the email; False otherwise
    :param client: boto3.client
    :param attachments: (list) of locations to add attachments from
    :param delete_attachments: (boolean) True if the attachments should be deleted afterwards
    :param with_cc: (list) of email addresses to send to as Cc
    :param with_bcc: (list) of email addresses to send to as Bcc
    '''
    logging.info('Emailing - ' + str(to_addr))
    if isinstance(to_addr, str):
        to_addr = [to_addr]
    try:
        if client is None:
            client = boto3.client('ses', region_name=sender_credentials.region,
                                  aws_access_key_id=sender_credentials.access_key,
                                  aws_secret_access_key=sender_credentials.secret_key)
        msg = MIMEMultipart()
        msg['Subject'] = subject
        msg['From'] = sender_credentials.email
        msg['To'] = ', '.join(to_addr)
        final_destinations = to_addr

        if with_cc is not None and isinstance(with_cc, list):
            msg['Cc'] = ', '.join(with_cc)
            final_destinations += with_cc
        if with_bcc is not None and isinstance(with_bcc, list):
            msg['Bcc'] = ', '.join(with_bcc)
            final_destinations += with_bcc
        msg.attach(MIMEText(message, 'html'))

        if attachments is not None and len(attachments) > 0:
            for attachment in attachments:
                with open(attachment, 'rb') as f:
                    part = MIMEApplication(f.read())
                    part.add_header('Content-Disposition', 'attachment', filename=os.path.basename(attachment))
                    msg.attach(part)

        response = client.send_raw_email(
            Destinations=final_destinations,
            RawMessage={
                'Data': msg.as_string()
            },
            Source=sender_credentials.email
        )
    except (OSError, IOError) as e:
        err = 'Could not read email addresses file.' + '\n' + str(e)
        raise OSError(err)
    except ClientError as e:
        logging.exception(e.response['Error']['Message'])
    except Exception:
        raise
    else:
        logging.info('Message ID: ' + response['MessageId'])
        if delete_attachments and attachments is not None and len(attachments) > 0:
            logging.info('Deleting attachments for this email')
            for item in attachments:
                os.remove(item)


class EmailAccountCredentials(object):

    def __init__(self, account_type='default'):
        self.account_type = account_type
        self.email, self.password, self.server = self.get_email_credentials(self.account_type)

    @staticmethod
    def get_email_credentials(account_type):
        '''
        Gets the email credentials for the account that emails will be sent out from
        :param account_type: the type of the email account
        :return: (tuple) -> email address, password, server
        '''
        bucket = 'taskcall-prod-data'
        key = 'credentials/email_addr.json'
        try:
            data = s3.read_json(bucket, key)
            email = data[account_type][var_names.email]
            password = data[account_type][var_names.password]
            server = data[account_type][var_names.server]
            return email, password, server
        except KeyError as e:
            err = 'Unknown email account type - ' + account_type + '\n' + str(e)
            raise SystemError(err)
        except (OSError, IOError) as e:
            err = 'Could not read email address credentials file' + '\n' + str(e)
            raise OSError(err)


class EmailAccountDispatcher(Thread):
    '''
    This is an email account mail dispatcher thread class. It should be used when many
    different emails need to be sent.
    '''

    def __init__(self, subject, message, to_addr, sender_credentials=None, with_logo=False):
        self.subject = subject
        self.message = message
        self.to_addr = to_addr
        self.with_logo = with_logo

        if sender_credentials is None:
            self.sender_credentals = EmailAccountCredentials()
        else:
            self.sender_credentals = sender_credentials
        Thread.__init__(self)

    def run(self):
        logging.info('Running email account dispatcher at ' +
                     times.get_current_timestamp().strftime(constants.timestamp_format))
        attempts = 0
        try:
            # make multiple attempts to send the email
            attempts += 1
            email_account_mailer(self.subject, self.message, self.to_addr, self.sender_credentals, self.with_logo)
        except Exception as e:
            if attempts <= 3:
                email_account_mailer(self.subject, self.message, self.to_addr, self.sender_credentals, self.with_logo)
            else:
                raise Exception(e)


def email_account_mailer(subject, message, to_addr, sender_credentials, with_logo=False):
    '''
    Sends email message from an email account.
    :param subject: subject of the email
    :param message: email body message
    :param to_addr: email address to send to
    :param sender_credentials: (EmailAccountCredentials object) the credentials of the email account
    :param with_logo: True if TaskCall logo should be loaded and sent with the email; False otherwise
    '''
    logging.info('Emailing - ' + str(to_addr))
    try:
        msg = MIMEMultipart()
        msg['Subject'] = subject
        msg['To'] = to_addr
        msg.attach(MIMEText(message, 'html'))

        # if with_logo:
        #     msg.attach(msg_image)

        server = smtplib.SMTP(sender_credentials.server)
        server.starttls()
        server.login(sender_credentials.email, sender_credentials.password)
        server.sendmail(sender_credentials.email, to_addr, msg.as_string())
        server.quit()
    except (OSError, IOError) as e:
        err = 'Could not read email addresses file.' + '\n' + str(e)
        raise OSError(err)
    except Exception as e:
        raise Exception(str(e))


def parse_task_from_email(msg):
    '''
    Parses an email file to recover the information about a new task.
    :param msg: email message
    :return: tuple -> email_from, email_to, title, description
    '''
    f = open('/tmp/ptfm.txt', 'w')
    f.write(str(msg))
    f.write('\n')

    title = get_decoded_email_subject(msg.get('Subject'))
    f.write(title)
    f.write('\n')

    email_from = get_email_from_identifier(msg.get('From'))
    email_to = extract_email_addresses(msg.get('To'))
    if 'X-Forwarded-To' in msg.keys():
        email_to += extract_email_addresses(msg.get('X-Forwarded-To'))
    if 'Cc' in msg.keys():
        email_to += extract_email_addresses(msg.get('Cc'))
    if 'Bcc' in msg.keys():
        email_to += extract_email_addresses(msg.get('Bcc'))
    if 'Received' in msg.keys():
        email_to += extract_email_addresses(msg.get('Received'))

    f.write(str(email_from))
    f.write('\n')
    f.write(str(msg.keys()))
    f.write('\n')
    f.write(str(email_to))
    f.write('\n')
    f.write(str(type(msg)))
    f.write('\n')

    encoding_type = None
    if 'Content-Transfer-Encoding' in msg.keys():
        encoding_type = msg.get('Content-Transfer-Encoding')

    description = get_email_body(msg, encoding_type)

    f.write(description)
    f.write('\n')
    f.write('Done')
    f.write('\n')
    f.close()

    return email_from, email_to, title, description


def extract_email_addresses(to_addr):
    '''
    Extract email addresses from the mail attribute values. It handles comma, new line and spaces.
    'To' and 'X-Forwarded-To' attribute values should be filtered and retrieved by this function.
    :param to_addr: (str) email addresses
    :return: (list) of email addresses
    '''
    filtered_addresses = []
    for item in [x.strip().replace(' ', '').split(",") for x in to_addr.splitlines()]:
        for sub_item in item:
            sub_item = get_email_from_identifier(sub_item)
            if string_validator.is_email_address(sub_item):
                filtered_addresses.append(sub_item)
    return filtered_addresses


def get_email_from_identifier(identifier):
    '''
    Get the email address from an email identifier like of the form 'name <email address>'
    :param identifier: (str) 'name <email address>'
    :return: (str) email address
    '''
    match = re.search('[a-zA-Z]*<(.*)>', identifier)
    if match is not None:
        return match.group(1)
    return identifier


def get_email_body(message, encoding_type):
    '''
    Gets the main body of an email.message object.
    :param message: email.message
    :param encoding_type: the type of encoding used for the email body
    :return: (str) body of the message
    '''
    fg = open('/tmp/gebt.txt', 'w')
    if isinstance(message, str):
        fg.write('step 1')
        fg.write('\n')
        fg.close()
        return get_decoded_email_content(message, encoding_type)

    msg_payload = message.get_payload()
    if isinstance(msg_payload, str):
        fg.write('step 2')
        fg.write('\n')
        fg.close()
        return get_decoded_email_content(msg_payload, encoding_type)
    elif isinstance(msg_payload, list):
        fg.write('step 3')
        fg.write('\n')
        while isinstance(msg_payload, list):
            fg.write('Step 3.1')
            fg.write('\n')
            msg_payload = msg_payload[0].get_payload()
        fg.write('Step 3 - final')
        fg.close()
        return get_decoded_email_content(msg_payload, encoding_type)
    else:
        fg.write('step 4')
        fg.write('\n')
        fg.close()
        return get_decoded_email_content(msg_payload[0].get_payload().__str__(), encoding_type)


def get_decoded_email_content(content, encoding_type):
    '''
    Decode the content of an email given an encoding type.
    :param content: email content
    :param encoding_type: (str) type of encoding
    :return: (str) decoded content
    '''
    if encoding_type is not None:
        try:
            if encoding_type == 'base64':
                return base64.b64decode(content.encode("utf-8")).decode("utf-8")
        except Exception as e:
            fe = open('/tmp/emer.txt', 'w')
            fe.write(str(encoding_type))
            fe.write('\n')
            fe.write(str(e))
            fe.close()
    return content


def get_decoded_email_subject(subject):
    '''
    Decode the subject of an email.
    :param subject: email subject
    :return: (str) decoded subject
    '''
    try:
        new_subject = str(make_header(decode_header(subject)))
        return new_subject
    except Exception as e:
        fe = open('/tmp/emer.txt', 'w')
        fe.write('\n')
        fe.write(str(e))
        fe.close()
        return subject


if settings.INITIALIZE_GLOBAL_VARIABLES:
    # Store in the image of the logo once. Don't reload it every time.
    image_filepaths = ['/var/www/html/taskcallrest/images/TaskCallCombinedLogo.png',
                       '/home/ubuntu/taskcallrest/images/TaskCallCombinedLogo.png',
                       r'C:\Users\MSI\PycharmProjects\taskcallrest\images\TaskCallCombinedLogo.png']
    found_path = False
    for image_path in image_filepaths:
        if not found_path and os.path.exists(image_path):
            fp = open(image_path, 'rb')
            msg_image = MIMEImage(fp.read())
            fp.close()
            msg_image.add_header('Content-ID', '<image1>')
            found_path = True

    if not found_path:
        raise FileNotFoundError('None of the image icon file paths provided exist...')

    # Create and store an amazon ses mail credentials object
    ses_credentials = AmazonSesCredentials()
else:
    msg_image = None
    ses_credentials = None
