# By: Riasat Ullah
# This class syncs up policy data stored in db with that in cache. In the case of retrievals, first attempt
# is made at the cache and then falls back to the db for cache misses.

from cache_queries import cache_policies
from dbqueries import db_policies, db_routing, db_workflows
from exceptions.user_exceptions import DependencyFound
from taskcallrest import settings
from utils import errors, key_manager, var_names


def get_policies(conn, client, timestamp, policy_ids, store_misses=True):
    '''
    Gets Policy objects.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param policy_ids: (list of int) of policy IDs
    :param store_misses: (boolean) True if cache misses that had to be retrieved
            from the db must be stored in cache afterwards; False otherwise
    :return: (dict) -> {pol id: Policy object, ...}
    :errors: AssertionError, DatabaseError, Redis errors
    '''
    retrieved_policies = dict()
    cache_miss_pol_ids = []

    if settings.CACHE_ON:
        retrieved_policies = cache_policies.get_policies(client, policy_ids)
        if len(retrieved_policies) != len(policy_ids):
            cache_miss_pol_ids = list(set(policy_ids).difference(set(retrieved_policies.keys())))
    else:
        cache_miss_pol_ids = policy_ids

    if len(cache_miss_pol_ids) > 0:
        policies_from_db = db_policies.get_policies(conn, timestamp, cache_miss_pol_ids)
        retrieved_policies = {**retrieved_policies, **policies_from_db}

        if store_misses and settings.CACHE_ON:
            cache_policies.store_policies(client, list(policies_from_db.values()))

    return retrieved_policies


def edit_policy(conn, client, timestamp, user_id, organization_id, policy_ref_id, policy_name, policy_levels: list,
                check_adv_perm=False, has_comp_perm=False, has_team_perm=False):
    '''
    Edits a policy.
    :param conn: db connection
    :param client: cache client
    :param timestamp: (datetime.datetime) timestamp when the group was created
    :param user_id: user_id of the user editing the details
    :param organization_id: (int) ID of the organization the policy belongs to
    :param policy_ref_id: (UUID) reference ID of the policy that needs to be changed
    :param policy_name: (string) the name of the group
    :param policy_levels: (list) of policy levels data
    :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: (int) policy ID
    :errors: AssertionError, DatabaseError, LookupError, PermissionError ValueError (from dependencies)
    '''
    pol_id = db_policies.edit_policy(conn, timestamp, user_id, organization_id,
                                     policy_ref_id, policy_name, policy_levels,
                                     check_adv_perm, has_comp_perm, has_team_perm)

    if settings.CACHE_ON:
        if cache_policies.is_policy_in_cache(client, pol_id):
            pol_obj = db_policies.get_policies(conn, timestamp, with_policyid=pol_id)[pol_id]
            cache_policies.store_single_policy(client, pol_obj)


def delete_policy(conn, client, timestamp, user_id, organization_id, policy_ref_id, check_adv_perm=False,
                  has_comp_perm=False, has_team_perm=False):
    '''
    Delete a policy. Edits all associated policies and tasks as needed.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param user_id: user_id of the user deleting the policy
    :param organization_id: ID of the organization the policy is for
    :param policy_ref_id: reference ID of the policy that is being deleted
    :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, DependencyError, LookupError, PermissionError
    '''
    print(client)
    unmasked_pol_ref = key_manager.unmask_reference_key(policy_ref_id)
    policy_id = db_policies.list_policy_ids_from_ref_ids(conn, timestamp, organization_id, [unmasked_pol_ref])[0]

    # If the policy is associated with any instances, tasks or services, then raise an error and abort deletion.
    pol_inst = db_policies.get_policy_open_org_instance_ids(conn, timestamp, policy_id)
    if len(pol_inst) > 0:
        raise DependencyFound(errors.err_policy_delete_incident_dependency,
                              ' - ' + ', '.join([str(x) for x in pol_inst]))

    pol_task = db_policies.get_policy_task_titles(conn, timestamp, policy_id)
    if len(pol_task) > 0:
        raise DependencyFound(errors.err_policy_delete_pre_scheduled_alert_dependency, ' - ' + '; '.join(pol_task))

    pol_serv = db_policies.get_policy_service_names(conn, timestamp, policy_id)
    if len(pol_serv) > 0:
        raise DependencyFound(errors.err_policy_delete_service_dependency, ' - ' + '; '.join(pol_serv))

    # check to see if any conditional routing is set to re-route to this policy and update it
    switched_routings = db_routing.get_component_removed_routing_actions(conn, timestamp, organization_id, policy_id,
                                                                         None, with_routing_name=True)
    if len(switched_routings) > 0:
        raise DependencyFound(errors.err_policy_delete_conditional_routing_dependency,
                              ' - ' + '; '.join([item[var_names.rule_name] for item in switched_routings]))

    # check to see if any workflow has this policy as a new responder
    switched_workflows = db_workflows.check_component_in_workflow(conn, timestamp, organization_id, pol_id=policy_id)
    if len(switched_workflows) > 0:
        raise DependencyFound(errors.err_workflow_dependency_policy_delete,
                              ' - ' + '; '.join([item[var_names.workflow_name] for item in switched_workflows]))

    db_policies.delete_policy(conn, timestamp, user_id, organization_id, policy_id,
                              check_adv_perm, has_comp_perm, has_team_perm)
    if settings.CACHE_ON:
        if cache_policies.is_policy_in_cache(client, policy_id):
            cache_policies.remove_policies(client, [policy_id], remove_refs=False)
            cache_policies.remove_policies_ref_map(client, [policy_ref_id])


def update_routine_associated_policies(conn, client, timestamp, pol_ids):
    '''
    When a routine is edited, updates the policies stored in cache that may be associated to that routine.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp to check on
    :param pol_ids: IDs of the policies to update
    '''
    if settings.CACHE_ON and len(pol_ids) > 0:
        stored_pols = cache_policies.get_policies(client, pol_ids)
        if len(stored_pols) > 0:
            updated_policies = list(db_policies.get_policies(
                conn, timestamp, with_policyid=list(stored_pols.keys())).values())
            cache_policies.store_policies(client, updated_policies)


def get_policy_ids_from_ref_ids(conn, client, timestamp, organization_id, policy_ref_ids):
    '''
    Get the policy ids of policies given their reference ids.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param organization_id: organization id the policies belong to
    :param policy_ref_ids: (list) of policy reference ids (unmasked)
    :return: (list) of int -> [policy id 1, policy id 2, ...]
    '''
    policy_ids = []
    cache_miss_ref_ids = []

    if settings.CACHE_ON:
        retrieved_maps = cache_policies.get_policy_ref_maps(client, policy_ref_ids)
        if len(retrieved_maps) > 0:
            policy_ids = [int(x) for x in list(retrieved_maps.values())]

        if len(retrieved_maps) != len(policy_ref_ids):
            cache_miss_ref_ids = list(set(policy_ref_ids).difference(set(retrieved_maps.keys())))
    else:
        cache_miss_ref_ids = policy_ref_ids

    if len(cache_miss_ref_ids) > 0:
        pol_ids_from_db = db_policies.list_policy_ids_from_ref_ids(conn, timestamp, organization_id, cache_miss_ref_ids)
        policy_ids += pol_ids_from_db

    return policy_ids


def get_policy_roles_to_hand_off(conn, client, timestamp, interval, ending_buffer, force_load_from_db=False):
    '''
    Gets all the policy roles that need to be handed off to someone else.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param interval: the number of minutes worth of data to pull
    :param ending_buffer: (int) minutes to buffer for ending on-call roles
    :param force_load_from_db: True if policies should be fetched from the database directly; False otherwise
    :return: (list) -> [{policy ID: , assignee level: , routine ID: , user_policyid: , data_type: }, {}]
        data_type is either 'upcoming' or 'ending'
    '''
    if settings.CACHE_ON and not force_load_from_db:
        return cache_policies.get_hand_off_on_call_roles(client)
    else:
        db_pols = db_policies.get_policy_roles_to_hand_off(conn, timestamp, interval, ending_buffer)

        if settings.CACHE_ON and force_load_from_db:
            if len(db_pols) > 0:
                cache_policies.store_hand_off_on_call_roles(client, db_pols)
            else:
                cache_policies.remove_all_hand_off_on_call_roles(client)

        return db_pols
