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

# This class represents a task monitor. It looks for tasks that need to be acted upon and
# dispatched. An instance of this class should be running at all times for each timezone.

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

from cache_queries import cache_task_instances
from dbqueries import db_tasks, db_task_instances
from taskcallrest import settings
from threading import Thread
from utils import constants, internal_alert_manager, logging, times, var_names
from utils.db_connection import CONN_POOL, CACHE_CLIENT
import argparse
import configuration
import datetime
import psycopg2
import time


class TaskMonitor(Thread):

    def __init__(self, conn, cache, task_time=None):
        self.conn = conn
        self.cache = cache
        if task_time is None:
            self.timestamp = times.get_current_timestamp()
        else:
            self.timestamp = task_time

        self.monitor_weekday = self.timestamp.weekday()
        self.monitor_time = self.timestamp.time()
        self.ex = None

        monitor_name = '_'.join(['TaskMonitor', str(self.monitor_weekday),
                                 str(self.monitor_time.hour), str(self.monitor_time.minute)])
        Thread.__init__(self, name=monitor_name)

    def run(self):
        '''
        Inserts new entries for task instances after finding the tasks
        that need to be alerted for at the timestamp the monitor was started on.
        '''
        logging.info(self.name + ' - Running monitor for ' + self.timestamp.strftime(constants.timestamp_format))
        logging.info(self.name + ' - Getting tasks...')
        try:
            current_tasks = db_tasks.get_tasks_to_trigger(self.conn, self.timestamp, self.monitor_time)
            if len(current_tasks) > 0:
                # filter by the regional weekday
                actionable_tasks = []
                for item in current_tasks:
                    repeat_on = item.details[var_names.repeat]
                    if (repeat_on is None or (repeat_on is not None and len(repeat_on) == 0)) and\
                            item.details[var_names.start_date] == self.timestamp.date():
                        actionable_tasks.append(item)
                    else:
                        task_weekday = self.monitor_weekday
                        if task_weekday is not None and\
                                item.details[var_names.task_timezone] != configuration.standard_timezone:
                            task_weekday = times.region_to_utc_time(
                                self.timestamp, item.details[var_names.task_timezone]
                            ).weekday()

                        if repeat_on is not None and task_weekday in repeat_on:
                            actionable_tasks.append(item)

                instance_count = db_task_instances.create_task_instances(self.conn, self.timestamp, actionable_tasks)
                if instance_count > 0:
                    # sync up with cache, otherwise these new instances will not get picked up by the instance monitor
                    task_insts = db_task_instances.get_instances(
                        self.conn, self.timestamp, active=True,
                        inst_task_ids=[item.taskid for item in actionable_tasks]
                    )
                    if len(task_insts) > 0:
                        cache_task_instances.store_multiple_instances(self.cache, list(task_insts.values()))

                    logging.info(self.name + ' - Instances created: ' + str(instance_count))
                else:
                    logging.info(self.name + ' - No scheduled tasks were found for this time')
            else:
                logging.info(self.name + ' - No scheduled tasks were found for this time')
        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()
    timestamp = args.timestamp
    dont_switch_to_current_time = args.dont_switch_to_current_time

    # pre text of error message for internal alerting
    pre_error_title = 'Task Monitor (' + settings.REGION + ')'

    assert (isinstance(timestamp, datetime.datetime) or isinstance(timestamp, str))
    if type(timestamp) is str:
        timestamp = datetime.datetime.strptime(timestamp, constants.timestamp_format)

    # Get the date of the monitor now. If the date changes through the course of the life of this
    # monitor, we will be able to pull changes from the database that may have happened.
    monitor_conn = CONN_POOL.get_db_conn()
    monitor_cache = CACHE_CLIENT
    monitor_date = timestamp.date()
    wait_seconds = 60
    stop = False
    while not stop:
        if timestamp.date() != monitor_date:
            monitor_date = timestamp.date()

        try:
            current_monitor = TaskMonitor(monitor_conn, monitor_cache, task_time=timestamp)
            current_monitor.start()
            current_monitor.join()

            time.sleep(wait_seconds)
        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)

        # When a test needs to be done from a non-current time
        if dont_switch_to_current_time:
            timestamp = timestamp + datetime.timedelta(seconds=wait_seconds)
        else:
            timestamp = times.get_current_timestamp()
