import os, re, datetime, warnings
from packaging import version

import markdown2

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.image import MIMEImage
from smtplib import SMTP, SMTP_SSL

import numpy as np
import pandas as pd
from pandas.io.formats.style import Styler
from bs4 import BeautifulSoup
import sass


def _standarise_email(obj):
    """Converts contact detail dictionaries into email strings"""
    if type(obj) is dict:
        obj = {k.lower(): v for k, v in obj.items()}
        fname = obj.get('fname', obj.get('firstname', obj.get('first_name', None)))
        lname = obj.get('lname', obj.get('lastname', obj.get('last_name', None)))
        fullname = obj.get('name', obj.get('fullname', obj.get('full_name', None)))
        email = obj.get('email', obj.get('emailaddress', obj.get('email_address')))

        if fname is not None and lname is not None:
            return f'{lname}, {fname} <{email}>'
        if fullname is not None:
            return f'{fullname} <{email}>'
        if fname is not None:
            return f'{fname} <{email}>'

        return f'{email}'
    return obj


class DataFrameToHTML:
    def __init__(self, df, index=True, header=True, index_names=False):
        if isinstance(df, Styler):
            self.styled_df = df
        else:
            self.styled_df = df.style
        self.df = self.styled_df.data

        self.has_index = index
        self.has_header = header
        self.has_index_names = index_names

        self.col_num_fmt = {}
        self.col_style = {}


    def col_num_fmt_auto(self):
        """Automatically detectes the number format of a column and formatts it"""

        # get all date columns
        for col_name, col_series in self.df.select_dtypes(include=["datetime", "datetimetz"]).items():
            self.col_num_fmt[col_name] = '{%Y-%m-%d}DATE'

        # get all numeric columns
        for col_name, col_series in self.df.select_dtypes(include=["number"]).items():
            self.col_styles(column=[col_name], classes=['text_align_right'])

            col_series_mod = col_series.replace(0, np.nan).abs()
            low = col_series_mod.quantile(0.2)
            high = col_series_mod.quantile(0.8)

            # check if percentages
            if -2 < low and high < 2:
                self.col_num_fmt[col_name] = '{:,.1f}%'
            # check if small number
            elif high < 1_000 and "int" not in str(col_series.dtype):
                self.col_num_fmt[col_name] = '{:,.0f}'
            # check if iso cob date
            elif 1900_00_00 < low and high < 2100_00_00:
                self.col_num_fmt[col_name] = '{:.0f}'
            # check if large number in millions
            elif low > 10_000_000:
                self.col_num_fmt[col_name] = '{:,.0f}'
            # else normal number format
            else:
                self.col_num_fmt[col_name] = '{:,.0f}'


    def col_num_fmt(self, column, num_format):
        """
        Set the number format of one or several columns.

        Args:
            column (str or list of str): column name e.g. 'RoE' or ['RoE', 'revenue']
            num_format (str): excel number format string e.g. '0.0%;[Red]-0.0%' or 'yyyy-mm-dd' or '#,##0'
        """
        if type(column) is str:
            column_lst = [column]
        for column in column_lst:
            self.col_num_fmt[column] = num_format

    def col_styles(self, column, classes=[], css_style={}):
        if type(column) is not list:
            column = [column]
        for col in column:
            if col not in self.col_style:
                self.col_style[col] = ''
            self.col_style[col] += '\n'.join([f'@extend .{i};' for i in classes]) + '\n' + str(css_style)[1:-1].replace(',', ';')


    def to_html(self, index=None, header=None, index_names=None):
        if index is not None:
            self.has_index=index
        if header is not None:
            self.has_header=header
        if index_names is not None:
            self.has_index_names=index_names
        to_html_kwargs = dict(index=self.has_index, header=self.has_header, index_names=self.has_index_names)

        # remove kwargs that are not comppatile with old pandas versions
        if version.parse(pd.__version__) < version.parse('1.5.0'):
            _ = to_html_kwargs.pop('index')
            _ = to_html_kwargs.pop('header')
            _ = to_html_kwargs.pop('index_names')
            warnings.warn(f'Pandas version "{pd.__version__}" does not support "index", "header", and "index_names" arguments - thus will be ignored.')


        # apply number formatting
        for col, fmt in self.col_num_fmt.items():
            try:
                if type(fmt) is str:
                    fmt_unit = fmt.split('}')[-1].lower()
                    nan_str = ''

                    # is percent - multipy by 100
                    if '%' in fmt_unit:
                        fmt_lambda = lambda num: nan_str if pd.isna(num) else str(fmt).format(num * 100)
                    # is date - use format string
                    elif 'date' in fmt_unit:
                        date_fmt = fmt.split('{')[1].split('}')[0]
                        fmt_lambda = lambda num: nan_str if pd.isna(num) else (num.strftime(date_fmt) if hasattr(num, 'strftime') else num)
                    # is billion - divide by 1_000_000_000
                    elif 'b' in fmt_unit or 'bn' in fmt_unit or 'bil' in fmt_unit or 'bln' in fmt_unit:
                        fmt_lambda = lambda num: nan_str if pd.isna(num) else (str(fmt).format(num / 1_000_000_000) if isinstance(num, int) or isinstance(num, float) else num)
                    # is million - divide by 1_000_000
                    elif 'm' in fmt_unit or 'mn' in fmt_unit or 'mio' in fmt_unit or 'mln' in fmt_unit:
                        fmt_lambda = lambda num: nan_str if pd.isna(num) else (str(fmt).format(num / 1_000_000) if isinstance(num, int) or isinstance(num, float) else num)
                    # is thousand - divide by 1_000
                    elif 'k' in fmt_unit or 'thsnd' in fmt_unit:
                        fmt_lambda = lambda num: nan_str if pd.isna(num) else (str(fmt).format(num / 1_000) if isinstance(num, int) or isinstance(num, float) else num)
                    # else - no division necessary
                    else:
                        fmt_lambda = lambda num: nan_str if pd.isna(num) else (str(fmt).format(num) if isinstance(num, int) or isinstance(num, float) else num)
                else:
                    fmt_lambda = fmt

                self.df[col] = self.df[col].apply(fmt_lambda)
            except Exception as e:
                warnings.warn(f'Error number formatting column "{col}": {e}')

        df_html = self.styled_df.to_html(**to_html_kwargs)

        style_html, table_html = df_html.split('</style>')
        style_html = re.sub(re.compile('<.*?>'), '', style_html)

        # add column styles
        for col, style in self.col_style.items():
            i = self.df.columns.to_list().index(col)
            style_html += f'#T_{self.styled_df.uuid} tbody .col{i} ' + '{\n' + f'{style}\n' + '}\n'

        return style_html, table_html


    def __str__(self):
        style_html, table_html = self.to_html()
        return f'<style>\n{style_html}\n</style>\n{table_html}'



def send_email(
        conn,
        from_,
        to_=[],
        cc_=[],
        bcc_=[],
        subject='',
        body_text='',
        body_html='',
        body_markdown='',
        add_sender_cc=False,
        attachments=[],
        embedded_imgs=[],
        dry_run=False,
        template='msft_plain_blue'
        ):

    # ensure recipients are lists
    if type(to_) is str:
        to_ = [to_]
    if type(cc_) is str:
        cc_ = [cc_]
    if type(bcc_) is str:
        bcc_ = [bcc_]

    if add_sender_cc:
        cc_ += [from_]

    # read-in template
    if template is None:
        template_html = '{body_html}'
    else:
        # standard template included in package
        if "." not in template:
            this_file_path = os.path.dirname(os.path.abspath(__file__))
            template = os.path.join(this_file_path, "templates", f"{template}.html")

        with open(template, "r") as file:
            template_html = file.read()


    # build email
    msg = MIMEMultipart("related")
    msg['Subject'] = subject
    msg['From'] = msg_from = _standarise_email(from_)

    if len(to_) > 0:
        msg['To'] = ','.join([_standarise_email(i) for i in to_])
    if len(cc_) > 0:
        msg['CC'] = ','.join([_standarise_email(i) for i in cc_])
    msg_to = ','.join(list(set([_standarise_email(i) for i in to_ + cc_ + bcc_])))

    if body_text != '':
        msg.attach(MIMEText(body_text, 'plain'))
    if body_markdown != '':
        body_html = markdown2.markdown(
            body_markdown,
            extras={
                'breaks': {'on_newline': True, 'on_backslash': True},
                'tables': {},
                'strike': {},
                'code-friendly': {},
                'fenced-code-blocks': {},
                'footnotes': {}
            })
    if body_html != '':
        body_html = template_html.replace('{body_html}', body_html)

        # combine style sheets
        soup = BeautifulSoup(body_html, features="lxml")
        style_html = ''
        for i, style in enumerate(soup.findAll('style')):
            style_html += style.encode_contents().decode("utf-8")
            style.decompose()
        head = soup.find('head')
        style = BeautifulSoup('<style>' + sass.compile(string=style_html) + '</style>', features="lxml")
        head.append(style)

        body_html = soup.prettify()

        msg.attach(MIMEText(body_html, 'html'))

    # embedd images
    for i, embed_i_path in enumerate(embedded_imgs):
        with open(embed_i_path, "rb") as file:
            embed_i = MIMEImage(file.read())
        embed_i['Content-ID'] = f'<image{i+1}>'
        msg.attach(embed_i)

    # add attchments
    for attach_i_path in attachments:
        attach_i_name = os.path.basename(attach_i_path)
        with open(attach_i_path, 'rb') as file:
            attach_i = MIMEApplication(file.read(), Name=attach_i_path)
        attach_i['Content-Disposition'] = f'attachment;filename="{attach_i_name}"'
        msg.attach(attach_i)

    # send email
    send_props = {
        'From': msg.get('From', None),
        'To': msg.get('To', None),
        'Cc': msg.get('CC', None),
        'Bcc': ','.join([_standarise_email(i) for i in bcc_]),
        'Subject': subject,
        'Body': body_html if body_html != '' else body_text,
        'Attachments': ',\n'.join(attachments),
        'Embedded': ',\n'.join(embedded_imgs)
        }
    try:
        if dry_run:
            send_props['Mode'] = 'DRYRUN'
            send_props['Status'] = 'SUCESS'
            send_props['Status Code'] = 0
        else: # real email
            send_props['Mode'] = 'REAL'
            conn.sendmail(msg_from, msg_to, msg.as_string())
            send_props['Status'] = 'SUCESS'
            send_props['Status Code'] = 1
    except Exception as e:
        send_props['Status'] = f'ERROR {e}'
    finally:
        send_props['Timestamp'] = datetime.datetime.now().isoformat()

        print(f"{send_props['Mode']} Email (Status: {send_props['Status']}) '{send_props['Subject']}' sent to: {msg_to}")
        return send_props






class Connection:
    """
    Class for connecting to smtp server.

    Example:
        ```python
        from beautifulemail import Connection, send_email

        with Connection('workbook.xlsx', mode='r', style='elegant_blue') as conn:
            ws1 = conn.to_excel(df1, sheetname='My Sheet', mode='a', startrow=0, startcol=0)
            ws2 = conn.to_excel(df2, sheetname='My Sheet', mode='a', startrow=20, startcol=0)
        ```
    """

    def __init__(
        self,
        host='',
        port=0,
        local_hostname=None,
        ssl=False,
        user=None,
        password=None,
        **kwargs,
    ):
        if ssl:
            self.conn = SMTP_SSL(host=host, port=port, local_hostname=local_hostname, **kwargs)
        else:
            self.conn = SMTP(host=host, port=port, local_hostname=local_hostname, **kwargs)

        if user is not None:
            self.conn.login(user=user, password=password)

        self.sent_emails = []


    def __enter__(self):
        if not hasattr(self, "conn"):
            raise Exception(
                "Wrong usage! Please run it this way:\n>>> from beautifulemail import Connection\n>>> with Connection('workbook.xlsx', mode='r', theme='elegant_blue') as conn:\n>>>     ws1 = writer.write_df(df1, sheetname='My Sheet'"
            )
        return self


    def __exit__(self, type, value, traceback):
        # ToDo: add exception handling here
        self.conn.close()
        self = None

    def send_email(self, *args, **kwargs):
        results = send_email(conn=self.conn, *args, **kwargs)
        self.sent_emails.append(results)
        return results

    def save_sent_email_summary(self, file_path):
        df = pd.DataFrame(self.sent_emails)
        if '.csv' in file_path:
            df.to_csv(file_path)
        elif '.xls' in file_path:
            df.to_excel(file_path)
        else:  # folder path
            file_path = os.path.join(file_path, 'sent_emails.xlsx')
            df.to_excel(file_path)




# Test Email
if __name__ == '__main__':

    EMAIL_HOST = os.environ.get('EMAIL_HOST')  # smtp.gmail.com
    EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))  # 465
    EMAIL_USER = os.environ.get('EMAIL_USERNAME')  # my_email@gmail.com
    EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD')  # my_secret_password

    example_df = pd.DataFrame(
        {
            "client": ["A", "B", "C", "D", "E", "F", "G", "H"],
            "industry": [
                "ASEET MANAGEMENT",
                "ASEET MANAGEMENT",
                "BANK",
                "INSURANCE",
                "VERY VERY VERY VERY VERY LONG INSUSTRY NAME",
                "BANK",
                "COMMODITY BROKER",
                "INSURANCE",
                ],
            "employees": [25_000, 17_000_000, 14, 9_000, 12_000_000, 9_000, np.nan, 4_000_000],
            "inception": [
                datetime.datetime(2022, 1, 1),
                datetime.datetime(2000, 10, 30),
                datetime.datetime(2010, 5, 6),
                np.nan,
                datetime.datetime(1997, 1, 1),
                datetime.datetime(1962, 1, 1),
                datetime.datetime(2003, 11, 10),
                datetime.datetime(2022, 1, 1),
                ],
            "last_contact": [
                datetime.datetime(2022, 1, 1, 10, 2, 5),
                datetime.datetime(2000, 10, 30),
                datetime.datetime(2010, 5, 6),
                np.nan,
                datetime.datetime(1997, 1, 1),
                datetime.datetime(1962, 1, 1),
                datetime.datetime(2003, 11, 10, 10, 2, 5),
                datetime.datetime(2022, 1, 1),
                ],
            "RoE": [0.05, -0.05, 0.15, 1.05, -0.02, np.nan, 0.08, 0.05],
            "revenue": [
                50_000_000.4387,
                np.nan,
                63_000_000.4387,
                25_000.4387,
                25_000_000.4387,
                -50_000_000.4387,
                76_000_000.4387,
                25_000.4387,
                ],
            }
        )
    example_df.set_index("client", inplace=True)

    df_styled = example_df.style.applymap(lambda i: '', subset=pd.IndexSlice[:, ['last_contact', 'revenue']])
    df_html = DataFrameToHTML(df=df_styled)
    df_html.col_num_fmt_auto()
    df_html.col_styles(column=['last_contact', 'revenue'], classes=['bg_light_blue'], css_style={})
    df_html.col_styles(column=['last_contact'], classes=['text_color_dark_red'])

    email_to = os.environ.get('EMAIL_TO', EMAIL_USER)  # my_email@gmail.com
    email_from = os.environ.get('EMAIL_FROM', EMAIL_USER)  # my_email@gmail.com
    email_subject = 'My Test'

    email_body_markdown = """
Hi,

This is a test email with **bold text**, *italic text*, ~~strikethrough text~~, <mark>highlited text</mark>, [hyperlink text](https://www.google.com), and text that could be footnoted<note>[1]</note>.

""" + str(df_html) + """

<br>
# This would be a Heading 1 of an ordered list

1. First step
2. Second step
3. Third step

<br>
## This would be a Heading 2 of an unordered list

- bullet point one
- bullet point two
- bullet point three

<br>
### This would be a Heading 3 of a table

| Syntax | Description |
| ----------- | ----------- |
| Header | Title |
| Paragraph | Text |

<br>
#### This would be a Heading 4 of a text blocks

Code block:
```python
{
  "firstName": "John",
  "lastName": "Smith",
  "age": 25
}
```

Text block/quote:
> This has to be a very
>  
> very important person's quote
  
Embedded image:
<img src="cid:image1" style="width: 100px;">

<br>
Best Wishes,
Me
<br>
**P.S.** The footnotes:
<footnote><note>[1]</note> Very important footnote</footnote>
    """

    with Connection(host=EMAIL_HOST, port=EMAIL_PORT, ssl=True, user=EMAIL_USER, password=EMAIL_PASSWORD) as conn:
        status = conn.send_email(
            from_=email_from,
            to_=email_to,
            subject=email_subject,
            body_markdown=email_body_markdown,
            attachments=['../README.md'],
            embedded_imgs=['../docs/docs/imgs/email_preview.jpg'],
            dry_run=False
            )

        with open('email_body_test.html', 'w') as file:
            file.write(status['Body'])
        conn.save_sent_email_summary('email_summary_test.xlsx')