# standard preamble
import logging
import datetime
import json
import re
from time import mktime

from matchminer.database import get_db
from matchminer.settings import TRIAL_SORT_DICT, EMAIL_AUTHOR_PROTECTED, EMAIL_TRIAL_CONTACT, EMAIL_TRIAL_CC_LIST, \
    TREATMENT_LIST_AUTO_UPDATE_KEYS

# logging
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s', )


class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return int(mktime(obj.timetuple()))
        return json.JSONEncoder.default(self, obj)


class OncoreSync(object):

    def __init__(self):

        now = datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")
        self.log_entries = list()
        self.email_body_head = '''
        <html>
        <head></head>
        <body>
        <ul>
        '''
        self.email_body_foot = '''
        </ul>
        -- This email was automatically generated by MatchMiner at %s.
        </br>
        </body>
        </html>
        ''' % now
        self.email_body = None

    @staticmethod
    def _smpl_diff(val1, val2):
        """custom comparator casts all values to lower case strings"""

        val1_str = str(val1).lower()
        val2_str = str(val2).lower()

        if val1_str < val2_str:
            return -1
        elif val1_str > val2_str:
            return 1
        else:
            return 0

    def _compare_recursive(self, oncore_dict, mm_dict, key_root):
        """
        Takes a dict with nested lists and dicts,
        and searches all dicts for a key of the field
        provided.
        """

        diffs = []
        for oncore_key, oncore_val in oncore_dict.items():

            if oncore_key not in mm_dict:
                continue
            mm_val = mm_dict[oncore_key]

            if isinstance(oncore_val, dict):
                diffs.extend(self._compare_recursive(oncore_val, mm_val, "%s.%s" % (key_root, oncore_key)))

            elif isinstance(oncore_val, list):

                if oncore_key == 'match':
                    continue

                # new items
                if len(oncore_val) > len(mm_val):
                    new_values = [item for item in oncore_val if item not in mm_val]
                    for item in new_values:
                        diffs.append({
                            'type': 'diff',
                            'key': '%s.%s' % (key_root, oncore_key),
                            'old': '',
                            'new': item
                        })
                    continue

                # deleted items
                elif len(mm_val) > len(oncore_val):
                    del_values = [item for item in mm_val if item not in oncore_val]
                    for item in del_values:
                        diffs.append({
                            'type': 'diff',
                            'key': '%s.%s' % (key_root, oncore_key),
                            'old': item,
                            'new': ''
                        })
                    continue

                # sort and compare remaining
                if oncore_key in TRIAL_SORT_DICT:
                    sortby = TRIAL_SORT_DICT[oncore_key]
                else:
                    sortby = list(oncore_val[0].keys())[0]

                # preserve sort order
                mm_vals_idx = [mm_val.index(i) for i in mm_val]
                mm_vals_dict = dict(list(zip([i[sortby] for i in mm_val], mm_vals_idx)))

                oncore_vals_sorted = sorted(oncore_val, key=lambda x: str(x[sortby]).lower())
                mm_vals_sorted = sorted(mm_val, key=lambda x: str(x[sortby]).lower())

                for ll_oncore_val, ll_mm_val, i in zip(oncore_vals_sorted, mm_vals_sorted,
                                                       list(range(len(oncore_vals_sorted)))):

                    if isinstance(ll_oncore_val, dict):
                        diffs.extend(
                            self._compare_recursive(ll_oncore_val, ll_mm_val, "%s.%s[%d]" % (
                                key_root, oncore_key, mm_vals_dict[ll_mm_val[sortby]]))
                        )

                    # don't deal with 2d lists
                    elif isinstance(ll_oncore_val, list):
                        raise NotImplementedError

                    else:
                        test = self._smpl_diff(ll_oncore_val, ll_mm_val)
                        if test != 0:
                            diffs.append({
                                'type': 'diff',
                                'key': '%s.%s' % (key_root, oncore_key),
                                'old': ll_mm_val,
                                'new': ll_oncore_val
                            })

            # do comparison of single value.
            else:
                test = self._smpl_diff(oncore_val, mm_val)
                if test != 0:
                    diffs.append({
                        'type': 'diff',
                        'key': '%s.%s' % (key_root, oncore_key),
                        'old': mm_val,
                        'new': oncore_val
                    })

        return diffs

    @staticmethod
    def ascii_encode_dict(data):
        ascii_encode = lambda x: x.encode('ascii', 'ignore') if isinstance(x, str) else x
        return dict(list(map(ascii_encode, pair)) for pair in list(data.items()))

    def build_email_and_log(self, protocol_no, event):
        """
        Builds the email body for the OnCore updates, but doesn't send them to the
        database until we've looped through all the trials.

        :param protocol_no: Protocol number for the trial that has been updated
        :param event: The update event
        :return:
        """

        if self.email_body is None:
            self.email_body = ''

        log_entry = dict()
        log_entry["protocol_no"] = protocol_no
        log_entry["key"] = event['key']
        log_entry["old"] = event['old']
        log_entry["new"] = event['new']
        log_entry["updated"] = datetime.datetime.now()
        self.log_entries.append(log_entry)
        self.email_body += '<br><li><b>%s</b> | key: %s, %s -> %s</li>' % (
            protocol_no,
            event['key'],
            str(event['old']),
            str(event['new'])
        )

    def send_email(self):
        """
        Directly inserts the email into the database. A background service is periodically
        running to check for these unset email collections
        """

        if self.email_body is None:
            return

        email_body = self.email_body_head + self.email_body + self.email_body_foot
        email_body = email_body.replace('\n', '')

        db = get_db()
        email_item = {
            'email_from': EMAIL_AUTHOR_PROTECTED,
            'email_to': EMAIL_TRIAL_CONTACT,
            'subject': "OnCore Trial Modifications",
            'body': email_body,
            'cc': EMAIL_TRIAL_CC_LIST,
            'sent': False,
            'num_failures': 0,
            'errors': []
        }
        db.email.insert(email_item)

    def compare_trials(self, on_trial, mm_trial):

        # strip all metadata and empty fields
        for key in list(mm_trial.keys()):
            if key[0] == "_":
                del mm_trial[key]

        # force encoding
        on_trial = json.loads(json.dumps(on_trial))
        mm_trial = json.loads(json.dumps(mm_trial, cls=MyEncoder))
        protocol_no = mm_trial['protocol_no']

        # oncore used to send data from their API in snakecase but changed to
        # camel case for unknown reasons 9/6/19
        on_trial = recursive_snake_case(on_trial)

        # loop over each top-level excluding match and priors:
        updated = list()
        for key in list(on_trial.keys()):

            if key in ["match", "prior_treatment_requirements"]:
                continue

            # special case for treatment list:
            # Appending to 'updated' list will programatically update the key.
            # Within the treatment list, we only want to do this for status keys.
            # Otherwise, we want to emit an email so the update can be performed manually.
            if key == "treatment_list":
                diffs = self._compare_recursive(on_trial[key], mm_trial[key], key)
                for v in diffs:

                    treatment_status = v['key'].split('.')[-1]
                    if treatment_status in TREATMENT_LIST_AUTO_UPDATE_KEYS:
                        v['type'] = 'status'
                        treatment_status_key_list = v['key'].split('.')
                        step_idx = int(treatment_status_key_list[1].split('[')[1].split(']')[0])
                        arm_idx = int(treatment_status_key_list[2].split('[')[1].split(']')[0])

                        if treatment_status == 'arm_suspended':
                            mm_trial['treatment_list']['step'][step_idx]['arm'][arm_idx]['arm_suspended'] = \
                                on_trial['treatment_list']['step'][step_idx]['arm'][arm_idx]['arm_suspended']

                        if treatment_status == 'level_suspended':
                            dose_idx = int(treatment_status_key_list[3].split('[')[1].split(']')[0])
                            mm_trial['treatment_list']['step'][step_idx]['arm'][arm_idx]['dose_level'][dose_idx][
                                'level_suspended'] = \
                                on_trial['treatment_list']['step'][step_idx]['arm'][arm_idx]['dose_level'][dose_idx][
                                    'level_suspended']

                        updated.append(v)
                    else:
                        self.build_email_and_log(protocol_no, v)

            # special case for single values.
            elif not isinstance(on_trial[key], dict):
                if key in mm_trial and OncoreSync._smpl_diff(on_trial[key], mm_trial[key]) != 0:
                    mm_trial[key] = on_trial[key]
                continue

            # perform comparison.
            else:

                # look for difference
                diffs = self._compare_recursive(on_trial[key], mm_trial[key], key)

                if len(diffs) != 0:
                    mm_trial[key] = on_trial[key]
                    for v in diffs:
                        updated.append(v)

        # strip all metadata and empty fields
        for key in list(mm_trial.keys()):

            # remove meta data
            if key[0] == "_":
                del mm_trial[key]

            # remove empty.
            if mm_trial[key] is None:
                del mm_trial[key]

        # log the modifictions.
        for event in updated:
            logging.info("trial changes: %s on key %s - %s -> %s" % (
            mm_trial['protocol_no'], event['key'], str(event['old']), str(event['new'])))

        # return the updated trial
        return mm_trial, len(updated) > 0, updated


def is_camelcase(s):
    return s != s.lower() and s != s.upper() and "_" not in s


def recursive_snake_case(on_trial):
    on_trial_snakecase = {}
    for key in list(on_trial.keys()):
        new_key = to_snake_case(key) if is_camelcase(key) else key
        on_trial_val = on_trial[key]
        if isinstance(on_trial_val, dict):
            on_trial_snakecase[new_key] = recursive_snake_case(on_trial_val)
        elif isinstance(on_trial_val, list):
            new_list = list()
            for val in on_trial_val:
                new_list.append(recursive_snake_case(val))
                on_trial_snakecase[new_key] = new_list
        else:
            on_trial_snakecase[new_key] = on_trial_val
    return on_trial_snakecase


def to_snake_case(val):
    return re.sub('([A-Z]+)', r'_\1',val).lower()
