# By: Riasat Ullah
# This class syncs up monitor checks data stored in db with that in cache.

from cache_queries import cache_heartbeats
from data_syncers import syncer_task_instances
from dbqueries.checks import db_checks, db_heartbeats
from objects.check import Check
from objects.events import ResolveEvent
from taskcallrest import settings
from utils import constants, logging


def create_check(conn, client, timestamp, organization_id, user_id, check_ref_id, check_type, check_name,
                 check_description, ping_type, ping_url, ping_email, ping_interval, grace_period, service_ref_id,
                 inc_title, inc_description, urgency_level, tags=None, ip_whitelist=None, email_whitelist=None,
                 packet_sizes=None, degraded_timeout=None, fail_timeout=None, check_ssl_expiry=None, ping_port=None,
                 additional_info=None, check_adv_perm=False, has_comp_perm=False, has_team_perm=False):
    '''
    Create a new monitor check.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param organization_id: ID of the organization this check is for
    :param user_id: ID of the user who is creating this check
    :param check_ref_id: reference ID of the check; only for heartbeat checks this will be pre-generated
                        and provided with the user request
    :param check_type: type of check (HEARTBEAT, etc)
    :param check_name: name for the check
    :param check_description: short description of what the check is for
    :param ping_type: type of ping method that will be used for the check (HTTP, EMAIL)
    :param ping_url: (for HTTP pings) url of the ping endpoint
    :param ping_email: (for email pings) email of the ping endpoint
    :param ping_interval: (integer) interval between pings
    :param grace_period: grace period allowed for successful pings
    :param service_ref_id: reference ID of the service this check is for
    :param inc_title: title of the incident that should be created if the check fails
    :param inc_description: description of the incident that should be created if the check fails
    :param urgency_level: urgency of the incident to be created if the check fails
    :param tags: (list of str) tags to be added to the incident that will be created if the check fails
    :param ip_whitelist: (list of IP addresses) IP addresses that HTTP requests will be allowed from
    :param email_whitelist: (list of email) email addresses that email pings will be allowed from
    :param packet_sizes: (list of int) packet sizes in kilobytes that should be used for the check
    :param degraded_timeout: (int) number of seconds to allow before a ping response is considered degraded
    :param fail_timeout: (int) number of seconds to allow before a ping response is considered to have failed
    :param check_ssl_expiry: (boolean) true if domain SSL expiry should be checked
    :param ping_port: (int) port to ping
    :param additional_info: (dict) additional information necessary for the check
    :param check_adv_perm: (boolean) should advanced permissions be checked
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :return: (concealed) check reference ID
    '''
    check_ref_id = db_checks.create_check(
        conn, timestamp, organization_id, user_id, check_ref_id, check_type, check_name, check_description,
        ping_type, ping_url, ping_email, ping_interval, grace_period, service_ref_id, inc_title,
        inc_description, urgency_level, tags=tags, ip_whitelist=ip_whitelist, email_whitelist=email_whitelist,
        packet_sizes=packet_sizes, degraded_timeout=degraded_timeout, fail_timeout=fail_timeout,
        check_ssl_expiry=check_ssl_expiry, ping_port=ping_port, additional_info=additional_info,
        check_adv_perm=check_adv_perm, has_comp_perm=has_comp_perm, has_team_perm=has_team_perm
    )
    if settings.CACHE_ON:
        if check_type == constants.heartbeat:
            hbt_obj = db_heartbeats.get_heartbeat(conn, timestamp, check_ref_id)
            if hbt_obj is not None:
                cache_heartbeats.store_single_heartbeat(client, hbt_obj)
    return check_ref_id


def edit_check(conn, client, timestamp, organization_id, user_id, check_ref_id, check_type, check_name,
               check_description, ping_type, ping_url, ping_email, ping_interval, grace_period, service_ref_id,
               inc_title, inc_description, urgency_level, tags=None, ip_whitelist=None, email_whitelist=None,
               packet_sizes=None, degraded_timeout=None, fail_timeout=None, check_ssl_expiry=None, ping_port=None,
               additional_info=None, check_adv_perm=False, has_comp_perm=False, has_team_perm=False):
    '''
    Edit an existing monitor check.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param organization_id: ID of the organization this check is for
    :param user_id: ID of the user who is creating this check
    :param check_ref_id: reference ID of the check
    :param check_type: type of check (HEARTBEAT, etc)
    :param check_name: name for the check
    :param check_description: short description of what the check is for
    :param ping_type: type of ping method that will be used for the check (HTTP, EMAIL)
    :param ping_url: (for HTTP pings) url of the ping endpoint
    :param ping_email: (for email pings) email of the ping endpoint
    :param ping_interval: (integer) interval between pings
    :param grace_period: grace period allowed for successful pings
    :param service_ref_id: reference ID of the service this check is for
    :param inc_title: title of the incident that should be created if the check fails
    :param inc_description: description of the incident that should be created if the check fails
    :param urgency_level: urgency of the incident to be created if the check fails
    :param tags: (list of str) tags to be added to the incident that will be created if the check fails
    :param ip_whitelist: (list of IP addresses) IP addresses that HTTP requests will be allowed from
    :param email_whitelist: (list of email) email addresses that email pings will be allowed from
    :param packet_sizes: (list of int) packet sizes in kilobytes that should be used for the check
    :param degraded_timeout: (int) number of seconds to allow before a ping response is considered degraded
    :param fail_timeout: (int) number of seconds to allow before a ping response is considered to have failed
    :param check_ssl_expiry: (boolean) true if domain SSL expiry should be checked
    :param ping_port: (int) port to ping
    :param additional_info: (dict) additional information necessary for the check
    :param check_adv_perm: (boolean) should advanced permissions be checked
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    '''
    db_checks.edit_check(
        conn, timestamp, organization_id, user_id, check_ref_id, check_type, check_name, check_description,
        ping_type, ping_url, ping_email, ping_interval, grace_period, service_ref_id, inc_title,
        inc_description, urgency_level, tags=tags, ip_whitelist=ip_whitelist, email_whitelist=email_whitelist,
        packet_sizes=packet_sizes, degraded_timeout=degraded_timeout, fail_timeout=fail_timeout,
        check_ssl_expiry=check_ssl_expiry, ping_port=ping_port, additional_info=additional_info,
        check_adv_perm=check_adv_perm, has_comp_perm=has_comp_perm, has_team_perm=has_team_perm
    )
    if settings.CACHE_ON:
        if check_type == constants.heartbeat:
            hbt_obj = db_heartbeats.get_heartbeat(conn, timestamp, check_ref_id)
            if hbt_obj is not None:
                cache_heartbeats.store_single_heartbeat(client, hbt_obj)


def delete_check(conn, client, timestamp, org_id, user_id, check_ref_id, check_type,
                 check_adv_perm=False, has_comp_perm=False, has_team_perm=False):
    '''
    Delete a monitor check.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param org_id: ID of the organization this check is for
    :param user_id: ID of the user who is creating this check
    :param check_ref_id: reference ID of the check
    :param check_type: type of check
    :param check_adv_perm: (boolean) should advanced permissions be checked
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :errors: AssertionError, DatabaseError, PermissionError
    '''
    db_checks.delete_check(
        conn, timestamp, org_id, user_id, check_ref_id,
        check_adv_perm=check_adv_perm, has_comp_perm=has_comp_perm, has_team_perm=has_team_perm
    )
    if settings.CACHE_ON:
        if check_type == constants.heartbeat:
            cache_heartbeats.remove_single_heartbeat(client, check_ref_id)


def enable_check(conn, client, timestamp, org_id, user_id, check_ref_id, check_type, to_enable,
                 check_adv_perm=False, has_comp_perm=False, has_team_perm=False):
    '''
    Enable/disable a monitor check.
    :param conn: db connection
    :param client: cache client
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param org_id: ID of the organization this check is for
    :param user_id: ID of the user who is creating this check
    :param check_ref_id: reference ID of the check
    :param check_type: type of check
    :param to_enable: True if the check should be enabled; False otherwise
    :param check_adv_perm: (boolean) should advanced permissions be checked
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    '''
    db_checks.enable_check(
        conn, timestamp, org_id, user_id, check_ref_id, to_enable,
        check_adv_perm=check_adv_perm, has_comp_perm=has_comp_perm, has_team_perm=has_team_perm
    )
    if settings.CACHE_ON:
        if check_type == constants.heartbeat:
            if not to_enable:
                cache_heartbeats.remove_single_heartbeat(client, check_ref_id)
            else:
                hbt_obj = db_heartbeats.get_heartbeat(conn, timestamp, check_ref_id)
                if hbt_obj is not None:
                    cache_heartbeats.store_single_heartbeat(client, hbt_obj)


def get_upcoming_heartbeats(conn, client, timestamp, forward_lookout, force_load_from_db=False):
    '''
    Gets all the heartbeats expected that are expected to run within the next forward lookout (minutes).
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param forward_lookout: (int) number of minutes to get the data for
    :param force_load_from_db: True if policies should be fetched from the database directly; False otherwise
    :return: (dict of dict) -> {id: Check object, ...}
    '''
    if settings.CACHE_ON and not force_load_from_db:
        return cache_heartbeats.get_all_heartbeats(client)
    else:
        db_hbt = db_heartbeats.get_upcoming_heartbeats(conn, timestamp, forward_lookout)
        if settings.CACHE_ON and force_load_from_db:
            if len(db_hbt) > 0:
                cache_heartbeats.store_heartbeats(client, list(db_hbt.values()))
            else:
                cache_heartbeats.remove_all_heartbeats(client)
        return db_hbt


def log_heartbeat(conn, client, timestamp, check_ref_id, ping_type, ip_addr=None, sender_email=None):
    '''
    Log a heartbeat.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param check_ref_id: (concealed) reference ID of the check
    :param ping_type: type of ping received (HTTP, EMAIL)
    :param ip_addr: (for HTTP pings) originating IP address
    :param sender_email: (for email pings) email address of the sender
    :errors: AssertionError, LookupError, PermissionsError
    '''
    if settings.CACHE_ON:
        hbt = cache_heartbeats.get_heartbeat(client, check_ref_id)
        if hbt is not None:
            if not hbt.ping_is_from_valid_source(ping_type, ip_addr, sender_email):
                raise PermissionError

            # get the current incident IDs before updating the status of the heartbeat
            # because the update removes all the incidents from the check object if the check passes
            curr_inc_ids = hbt.incidents
            org_id = hbt.organization_id
            hbt.update_status(timestamp, has_passed=True)
            cache_heartbeats.store_single_heartbeat(client, hbt)
        else:
            org_id, curr_inc_ids = db_heartbeats.log_heartbeat(conn, timestamp, check_ref_id, ip_addr, sender_email)
            hbt_obj = db_heartbeats.get_heartbeat(conn, timestamp, check_ref_id)
            if hbt_obj is not None:
                cache_heartbeats.store_single_heartbeat(client, hbt_obj)
    else:
        org_id, curr_inc_ids = db_heartbeats.log_heartbeat(conn, timestamp, check_ref_id, ip_addr, sender_email)

    if org_id is not None and curr_inc_ids is not None:
        try:
            for inst_id in curr_inc_ids:
                event = ResolveEvent(inst_id, timestamp, constants.check)
                syncer_task_instances.resolve(conn, client, event, org_id, is_sys_action=True)
        except Exception as e:
            logging.info(str(e))
