#!/usr/bin/env python3
# By: Riasat Ullah

# This class represents an on-call notifier. It notifies users when they are going on-call
# as well as when they come off their on-call schedule.

import sys
sys.path.append('/var/www/html/taskcallrest/')

from cache_queries import cache_policies
from data_syncers import syncer_policies
from modules.notice_allocator import NoticeAllocator
from notices import user_notices
from taskcallrest import settings
from threading import Thread
from utils import app_notice, constants, internal_alert_manager, logging, mail, times, var_names
from utils.db_connection import CACHE_CLIENT, CONN_POOL
import argparse
import datetime
import psycopg2
import time


class OnCallNotifier(Thread):

    def __init__(self, conn, cache_client, email_creds, monitor_time=None, interval_minutes=10,
                 ending_buffer_minutes=5, load_handoff_from_db=False):
        self.conn = conn
        self.cache_client = cache_client
        self.email_creds = email_creds
        self.monitor_time = times.get_current_timestamp() if monitor_time is None else monitor_time
        self.interval = interval_minutes
        self.ending_buffer = ending_buffer_minutes
        self.load_handoff_from_db = load_handoff_from_db

        self.hand_off_roles = []
        self.notifier = NoticeAllocator()
        self.ex = None

        Thread.__init__(self)

    def set_up_environment(self):
        logging.info('Setting up the environment...')
        if self.load_handoff_from_db:
            logging.info('Fetching hand-off roles from the database...')
        self.hand_off_roles = syncer_policies.get_policy_roles_to_hand_off(
            self.conn, self.cache_client, self.monitor_time, self.interval, self.ending_buffer,
            force_load_from_db=self.load_handoff_from_db)

        if len(self.hand_off_roles) > 0:
            self.notifier = NoticeAllocator()

    def run(self):
        '''
        Notifies the users who are going on-call and those who are coming off their on-call schedule.
        '''
        logging.info('Running On-Call Handoff Notifier')
        try:
            self.set_up_environment()
            if len(self.hand_off_roles) == 0:
                logging.info('No possible handoff on-call roles were found')
            else:
                logging.info('Found handoff on-call roles - ' + str(len(self.hand_off_roles)))
                remaining_hand_offs = []

                for item in self.hand_off_roles:
                    matched = False
                    false_positive = False
                    rou_obj = item[var_names.routines]
                    assignee_pid = item[var_names.user_policyid]
                    hand_off_time = item[var_names.handoff_timestamp]
                    push_tokens = item[var_names.push_token]
                    minutes_till_handoff = (item[var_names.handoff_timestamp] - self.monitor_time).total_seconds() / 60

                    if item[var_names.data_type] == var_names.upcoming:
                        if rou_obj.is_assignee_going_on_call(assignee_pid, hand_off_time):
                            on_call_period = rou_obj.get_assignee_on_call_period(assignee_pid, hand_off_time)[0]
                            if on_call_period is not None:
                                if 0 <= minutes_till_handoff <= item[var_names.minutes_buffer]:
                                    matched = True
                                    em_sub, em_msg = user_notices.upcoming_on_call_email_content(
                                        item[var_names.language], item[var_names.display_name],
                                        item[var_names.policy_name], item[var_names.assignee_level], on_call_period[0],
                                        on_call_period[1], rou_obj.routine_timezone
                                    )
                                    self.notifier.handle_email_dispatch(
                                        em_sub, em_msg, item[var_names.email], [item[var_names.user_id]],
                                        organization_id=item[var_names.organization_id]
                                    )

                                    if push_tokens is not None and len(push_tokens) > 0:
                                        p_ttl, p_msg = user_notices.upcoming_on_call_push_notice(
                                            item[var_names.language], item[var_names.policy_name],
                                            item[var_names.assignee_level], on_call_period[0], on_call_period[1],
                                            rou_obj.routine_timezone
                                        )
                                        self.notifier.handle_app_dispatch(
                                            p_ttl, p_msg, tokens=push_tokens, user_id=item[var_names.user_id],
                                            organization_id=item[var_names.organization_id]
                                        )
                            else:
                                false_positive = True
                        else:
                            false_positive = True

                    elif item[var_names.data_type] == var_names.ending:
                        if rou_obj.is_assignee_coming_off_on_call(assignee_pid, hand_off_time):
                            if 0 <= minutes_till_handoff <= self.ending_buffer:
                                matched = True
                                region_end = times.utc_to_region_time(hand_off_time, rou_obj.routine_timezone)
                                em_sub, em_msg = user_notices.ending_on_call_email_content(
                                    item[var_names.language], item[var_names.display_name], item[var_names.policy_name],
                                    item[var_names.assignee_level], region_end, rou_obj.routine_timezone
                                )
                                self.notifier.handle_email_dispatch(
                                    em_sub, em_msg, item[var_names.email], [item[var_names.user_id]],
                                    organization_id=item[var_names.organization_id]
                                )

                                if push_tokens is not None and len(push_tokens) > 0:
                                    p_ttl, p_msg = user_notices.ending_on_call_push_notice(
                                        item[var_names.language], item[var_names.policy_name],
                                        item[var_names.assignee_level], region_end, rou_obj.routine_timezone
                                    )
                                    self.notifier.handle_app_dispatch(
                                        p_ttl, p_msg, tokens=push_tokens, user_id=item[var_names.user_id],
                                        organization_id=item[var_names.organization_id]
                                    )
                        else:
                            false_positive = True

                    if not matched and not false_positive:
                        remaining_hand_offs.append(item)

                if len(self.notifier.email_messages) > 0:
                    logging.info('Sending email messages: ' + str(len(self.notifier.email_messages)))
                    mail.AmazonSesBulkDispatcher(self.notifier.email_messages, self.email_creds).start()

                if len(self.notifier.push_notices) > 0:
                    logging.info('Sending push notices: ' + str(len(self.notifier.push_notices)))
                    app_notice.NoticeSender(self.notifier.push_notices).start()

                if len(remaining_hand_offs) > 0:
                    cache_policies.store_hand_off_on_call_roles(self.cache_client, remaining_hand_offs)
                else:
                    cache_policies.remove_all_hand_off_on_call_roles(self.cache_client)
        except psycopg2.InterfaceError as e:
            logging.error('Error from inner scope')
            logging.exception(str(e))
            self.ex = e
        except Exception as e:
            logging.error('Error from inner scope')
            logging.exception(str(e))
            self.ex = e

    def join(self):
        Thread.join(self)
        # Since join() returns in caller thread we re-raise the caught exception if any was caught
        if self.ex:
            raise self.ex


if __name__ == '__main__':
    arg_parser = argparse.ArgumentParser()
    arg_parser.add_argument('--timestamp', type=str,
                            default=times.get_current_timestamp())
    arg_parser.add_argument('--dont_switch_to_current_time', action='store_true')
    args = arg_parser.parse_args()
    start_time = args.timestamp
    dont_switch_to_current_time = args.dont_switch_to_current_time

    # pre text of error message for internal alerting
    pre_error_title = 'On-Call Handoff Notifier (' + settings.REGION + ')'

    if start_time is not None:
        assert (isinstance(start_time, datetime.datetime) or isinstance(start_time, str))
        if type(start_time) is str:
            start_time = datetime.datetime.strptime(start_time, constants.timestamp_format)
    else:
        start_time = times.get_current_timestamp()

    monitor_conn = CONN_POOL.get_db_conn()
    monitor_cache = CACHE_CLIENT
    monitor_date = start_time.date()

    # get the email account credentials
    email_credentials = mail.AmazonSesCredentials()

    # wait time in seconds before next run, refresh minutes for how often text/call usage costs
    # should be refreshed and cleanup minutes for how often the cache should be cleaned up
    wait_seconds = 60
    refresh_minutes = 8

    # We set the last_refresh earlier than start time so that a
    # refresh happens immediately upon starting the monitor
    stop = False
    timestamp = start_time
    force_handoff_load_from_db = True
    last_refresh = timestamp

    # Clean the current hand off roles in cache completely
    cache_policies.remove_all_hand_off_on_call_roles(monitor_cache)

    while not stop:
        # If the date changes then get the latest dispatch layers.
        if timestamp.date() != monitor_date:
            monitor_date = timestamp.date()
            email_credentials = mail.AmazonSesCredentials()
        try:
            current_notifier = OnCallNotifier(monitor_conn, monitor_cache, email_credentials, monitor_time=timestamp,
                                              load_handoff_from_db=force_handoff_load_from_db)
            current_notifier.start()
            current_notifier.join()

            time.sleep(wait_seconds)
            if dont_switch_to_current_time:
                timestamp = timestamp + datetime.timedelta(seconds=wait_seconds)
            else:
                timestamp = times.get_current_timestamp()

            if (timestamp - last_refresh).seconds / 60 > refresh_minutes:
                force_handoff_load_from_db = True
                last_refresh = timestamp
            else:
                force_handoff_load_from_db = False

        except psycopg2.InterfaceError as e:
            logging.error('Outer scope - possible connection error')
            logging.exception(str(e))
            internal_alert_manager.dispatch_alerts(pre_error_title + ' - Connection Error', str(e))
            try:
                CONN_POOL.put_db_conn(monitor_conn)
            except Exception as e:
                logging.exception(str(e))
            finally:
                logging.info('Trying to open a new connection')
                monitor_conn = CONN_POOL.get_db_conn()
            sys.exit(1)
        except Exception as e:
            logging.info('Outer scope - unknown error')
            logging.exception(str(e))
            internal_alert_manager.dispatch_alerts(pre_error_title + ' - Unknown Error', str(e))
            sys.exit(1)
