#!/usr/bin/env python3
# By: Riasat Ullah
# This class represents the heartbeat monitor job. It runs and tracks the outcome of all type of monitor/ping checks.

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

from cache_queries import cache_heartbeats
from data_syncers import syncer_checks
from dbqueries.checks import db_heartbeats
from modules.router import Router
from objects.check import Check
from objects.task_payload import TaskPayload
from taskcallrest import settings
from threading import Thread
from utils import constants, internal_alert_manager, logging, times, var_names
from utils.db_connection import CACHE_CLIENT, CONN_POOL
import argparse
import configuration
import datetime
import psycopg2
import time


class HeartbeatMonitor(Thread):

    def __init__(self, conn, cache_client, monitor_time=None, forward_lookout=5, load_checks_from_db=False):
        self.conn = conn
        self.cache_client = cache_client
        self.monitor_time = times.get_current_timestamp() if monitor_time is None else monitor_time
        self.forward_lookout = forward_lookout
        self.load_checks_from_db = load_checks_from_db

        self.checks = []
        self.ex = None

        Thread.__init__(self)

    def set_up_environment(self):
        logging.info('Setting up the environment...')
        if self.load_checks_from_db:
            logging.info('Fetching heartbeats from the database...')
        self.checks = syncer_checks.get_upcoming_heartbeats(
            self.conn, self.cache_client, self.monitor_time, self.forward_lookout,
            force_load_from_db=self.load_checks_from_db
        )

    def run(self):
        '''
        Runs the heartbeat checks.
        '''
        logging.info('Running Heartbeat Monitor')
        try:
            self.set_up_environment()
            if len(self.checks) == 0:
                logging.info('No heartbeat checks were found that are expected to run now')
            else:
                logging.info('Found heartbeat checks that are expected to run - ' + str(len(self.checks)))
                remaining_checks = []
                db_update_params = []

                for check_id in self.checks:
                    chk = self.checks[check_id]

                    if chk.is_enabled and chk.has_heartbeat_check_failed(self.monitor_time):
                        logging.info('Check Failed - ' + str(chk.check_ref_id))
                        payload = TaskPayload(
                            self.monitor_time, chk.organization_id, self.monitor_time.date(), chk.task_title,
                            configuration.standard_timezone, self.monitor_time.time(), text_msg=chk.text_msg,
                            urgency_level=chk.urgency_level, trigger_method=constants.check,
                            trigger_info=chk.get_incident_alert_body(), service_id=chk.service_id,
                            tags=chk.tags, dedup_key=chk.check_name + str(chk.check_ref_id)
                        )
                        inst_id = Router(self.conn, self.cache_client, payload).run()
                        chk.update_status(self.monitor_time, has_passed=False, instance_id=inst_id)

                    # The time before which the cache gets updated from the database is actually outside the
                    # scope of this class and will actually lessen every time the monitor is run until the
                    # refresh happens. We are using an estimate for the maximum time it could take based
                    # on the forward lookout.
                    max_time_before_db_refresh = self.monitor_time + datetime.timedelta(
                        minutes=self.forward_lookout)
                    if chk.next_run < max_time_before_db_refresh:
                        remaining_checks.append(chk.check_id)

                    if chk.events is not None and len(chk.events) > 0:
                        for log in chk.events:
                            db_update_params.append(
                                (chk.check_id, log[var_names.passed], log[var_names.run_timestamp],
                                 log[var_names.scheduled_timestamp], chk.next_run, log[var_names.instance_id],)
                            )

                # Upload stored heartbeat pings
                if len(db_update_params) > 0:
                    db_heartbeats.upload_heartbeat_logs(self.conn, db_update_params)
                    for item in db_update_params:
                        self.checks[item[0]].events = None

                if len(remaining_checks) > 0:
                    cache_heartbeats.store_heartbeats(self.cache_client, [self.checks[id_] for id_ in remaining_checks])
                else:
                    cache_heartbeats.remove_all_heartbeats(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 = 'Heartbeat Monitor (' + 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()

    # 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 = 30
    refresh_minutes = 4

    # 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_checks_load_from_db = True
    last_refresh = timestamp

    # Clean the current checks in cache completely
    cache_heartbeats.remove_all_heartbeats(monitor_cache)

    while not stop:
        if timestamp.date() != monitor_date:
            monitor_date = timestamp.date()
        try:
            current_monitor = HeartbeatMonitor(monitor_conn, monitor_cache, monitor_time=timestamp,
                                               load_checks_from_db=force_checks_load_from_db)
            current_monitor.start()
            current_monitor.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_checks_load_from_db = True
                last_refresh = timestamp
            else:
                force_checks_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)
