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

from cache_queries import cache_task_instances
from data_syncers import syncer_organizations, syncer_policies, syncer_services
from dbqueries import db_events, db_policies, db_tasks, db_task_instances
from exceptions.user_exceptions import MaliciousSource, UnauthorizedRequest
from integrations import microsoft_teams, webex, zoom
from modules.workflow_manager import WorkflowManager
from objects.assignee import Assignee
from objects.events import AddConferenceBridgeEvent, TriggerEvent
from objects.instance_state import InstanceState
from objects.task import Task
from taskcallrest import settings
from utils import constants, errors, helpers, logging, integration_type_names as intt, permissions, var_names
import configuration


def create_task(conn, client, timestamp, organization_id, start_date, title, timezone, task_time, service_id=None,
                service_policy_id=None, assignees=None, created_by=None, repeat_on=None, text_msg=None,
                urgency_level=configuration.default_urgency_level, trigger_method=constants.email, trigger_info=None,
                instantiate=True, alert=True, integration_id=None, api_key_id=None, routing_id=None,
                related_task_id=None, task_status=None, notes=None, tags=None, dedup_key=None, pre_scheduled=False,
                amended_urgency=None, next_alert_timestamp=None, status_update=None, subscribers=None,
                conference_bridge=None, impacted_business_services=None):
    '''
    Creates a new task in the database and then stores it in the cache.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when the task was created
    :param organization_id: ID of the organization this task is for
    :param start_date: date the task should start from
    :param title: title of the task
    :param timezone: timezone the task is being created for
    :param task_time: time the task should trigger at in the format HH:MM
    :param service_id: ID of the service this task has been associated to
    :param service_policy_id: policy ID the service is associated to
    :param assignees: (list of int) of policy ids of assignees (from service, users, groups)
    :param created_by: (can be None) user_id of the user who created the task
    :param repeat_on: (list) of days the task should be repeated on
    :param text_msg: text message of the task
    :param urgency_level: urgency level of the task
    :param trigger_method: the method used to trigger the task creation [APP, EMAIL, API, etc]
    :param trigger_info: additional information that may have been provided with the trigger
    :param instantiate: (boolean) True if instances should be created for this task
    :param alert: (boolean) True if alerts should be generated from the instances created this task
    :param integration_id: ID of the integration that created the task
    :param api_key_id: ID of the API key that created the instance
    :param routing_id: conditional routing ID
    :param related_task_id: ID of any other task this task is related to. This will be useful for grouping/suppressing
    :param task_status: a status of the task - GROUPED, SUPPRESSED, etc
    :param notes: any notes that were added to the task
    :param tags: list of tags to help classify a task
    :param dedup_key: a key to stop de-duplication; if this key is present in active task instance, then instances
                    will not be created for subsequent tasks containing the key
    :param pre_scheduled: True if the task is pre_scheduled; False otherwise
    :param amended_urgency: (integer) if priority is not internally changed then this stays as None
    :param next_alert_timestamp: timestamp the alert should be re-triggered next
    :param status_update: (str) status update to add to the instance
    :param subscribers: (list of user_ids) subscribers to add to the instance
    :param conference_bridge: (dict) details of conference bridge to add to the instance
    :param impacted_business_services: (list of int) IDs of business services that will eb impacted by this instance
    :return: (tuple) -> (task ID, inst ID, org inst ID, inst assignees)
            if task is pre-scheduled or is not to be instantiated then inst ID and inst assignees will be None
    :errors: AssertionError, DatabaseError
    '''
    # Check to make sure that the organization creating the instance is not blacklisted
    if syncer_organizations.is_organization_blacklisted(conn, client, timestamp, organization_id):
        raise MaliciousSource(errors.err_org_malicious_activity + ' - ' + str(organization_id))

    pol_ids = []
    instance_assignments = None
    impacted_bus_serv_ids = None
    avl_level = 1
    if assignees is not None:
        pol_ids += assignees

    # Only if an instance needs to be created immediately with the task
    if instantiate and not pre_scheduled:

        if service_id is not None:
            if service_policy_id is not None:
                pol_ids.append(service_policy_id)
            else:
                raise SystemError(errors.err_internal_service_policy_id_missing)

        pol_ids = list(set(pol_ids))
        if len(pol_ids) == 0:
            raise ValueError(errors.err_select_at_least_one_assignee)

        policy_objects = syncer_policies.get_policies(conn, client, timestamp, pol_ids, store_misses=True)
        instance_assignments = []
        for id_ in pol_ids:
            if id_ not in policy_objects:
                raise LookupError(errors.err_unknown_resource)

            assignee_policy = policy_objects[id_]

            # Ensure that the policies belong to the organization. This is the only place where this
            # cross-checking is done. The cross-checking for service is done in the router only. It
            # is okay to not check for the service here again because if the policy associated to the
            # service is checked as it is being checked, then the service is inherently also being checked.
            if assignee_policy.organization_id != organization_id:
                raise UnauthorizedRequest(errors.err_unknown_resource)

            avl_level, avl_assignments = helpers.get_new_instance_assignments_struct(timestamp, assignee_policy)
            instance_assignments += avl_assignments

        if next_alert_timestamp is None:
            next_alert_timestamp = InstanceState.calculate_next_alert_timestamp(
                avl_level, list(policy_objects.values()), timestamp)

        if impacted_business_services is not None:
            impacted_bus_serv_ids = [x[0] for x in impacted_business_services]

    task_id, inst_id, org_inst_id, inst_assignees = db_tasks.create_task(
        conn, timestamp, organization_id, start_date, title, timezone, task_time, service_id, service_policy_id,
        assignees, created_by, repeat_on, text_msg, urgency_level, trigger_method, trigger_info, instantiate, alert,
        integration_id, api_key_id, routing_id, related_task_id, task_status, notes, tags, dedup_key, pre_scheduled,
        amended_urgency, next_alert_timestamp, status_update, subscribers, conference_bridge, impacted_bus_serv_ids,
        instance_assignments, avl_level
    )

    # Do not put the instance in cache if it has already been set to be automatically resolved by a conditional routing.
    if settings.CACHE_ON and task_status != constants.resolved_state:
        if inst_id is not None:
            task_details = {var_names.task_id: task_id,
                            var_names.created_by: created_by,
                            var_names.task_title: title,
                            var_names.task_timezone: timezone,
                            var_names.task_time: task_time,
                            var_names.text_msg: text_msg,
                            var_names.current_version: True,
                            var_names.urgency_level: urgency_level,
                            var_names.to_alert: alert,
                            var_names.integration_id: integration_id,
                            var_names.related_task_id: related_task_id,
                            var_names.status: task_status,
                            var_names.service_id: service_id}

            task_labels = {var_names.tags: tags, var_names.dedup_key: dedup_key}
            inst_task = Task(task_id, task_details, None, task_labels)

            inst_assignee_objects = [Assignee.create_assignee(assig_item) for assig_item in inst_assignees]\
                if inst_assignees is not None else None

            instance = InstanceState(inst_id, org_inst_id, organization_id, timestamp, timestamp, avl_level, timestamp,
                                     next_alert_timestamp, constants.open_state, inst_assignee_objects, inst_task,
                                     [TriggerEvent(inst_id, timestamp, trigger_method, next_alert_timestamp)],
                                     subscribers=subscribers, impacted_business_services=impacted_business_services)

            cache_task_instances.store_single_instance(client, instance)

    return task_id, inst_id, org_inst_id, inst_assignees


def get_instances_to_monitor(conn, client, timestamp, force_load_from_db=False):
    '''
    Gets all the instances that need to be monitored.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param force_load_from_db: True if the instances should be fetched from the database directly; False otherwise
    :return: (dict) -> {inst ID: InstanceState object, ...}
    '''
    if settings.CACHE_ON and not force_load_from_db:
        return cache_task_instances.get_open_instances(client)
    else:
        db_insts = db_task_instances.get_active_instances_for_monitoring(conn, timestamp)

        if force_load_from_db:
            cache_task_instances.remove_all_instances(client)

        if len(db_insts) > 0 and settings.CACHE_ON:
            cache_task_instances.store_multiple_instances(client, list(db_insts.values()))

        return db_insts


def get_single_instance(conn, client, timestamp, inst_id, store_misses=True):
    '''
    Gets a single instance that needs to be monitored or re-monitored.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param inst_id: ID of the instance to get
    :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: InstanceState object
    '''
    fetched_inst = None
    cache_missed = False

    if settings.CACHE_ON:
        fetched_inst = cache_task_instances.get_single_instance(client, inst_id)
        if fetched_inst is None:
            cache_missed = True
    else:
        cache_missed = True

    if cache_missed:
        retrieved_instances = db_task_instances.get_alert_agnostic_active_instances_for_monitoring(
            conn, timestamp, [inst_id])
        if len(retrieved_instances) > 0:
            fetched_inst = retrieved_instances[inst_id]

        if store_misses and settings.CACHE_ON and fetched_inst is not None:
            cache_task_instances.store_single_instance(client, fetched_inst)

    return fetched_inst


def acknowledge(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False,
                skip_syncing=None, org_perm=None):
    '''
    Acknowledge a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: AcknowledgeEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :param skip_syncing: (list) of integration types to skip
    :param org_perm: organization permissions
    :return: (timestamp) next alert timestamp
    :errors: AssertionError, DatabaseError, LookupError
    '''
    try:
        updated_next_alert = db_events.book_acknowledge_event(conn, event, org_id, has_comp_perm, has_team_perm,
                                                              is_sys_action)
    except LookupError:
        logging.info(errors.err_unknown_resource + ' | Could not acknowledge Incident #' + str(event.instance_id))
        if settings.CACHE_ON:
            logging.info('Removing from cache: Incident #' + str(event.instance_id))
            cache_task_instances.remove_instance(client, event.instance_id)
        raise

    # Handle synced instance actions
    syncer_services.run_integration_syncer(conn, client, event.event_timestamp, org_id, event.instance_id,
                                           event_by=event.event_by, skip_syncing=skip_syncing,
                                           new_status=constants.acknowledged_state, org_perm=org_perm)

    if org_perm is not None and permissions.has_org_permission(org_perm, permissions.ORG_WORKFLOWS_PERMISSION):
        WorkflowManager(conn, client, org_id, org_perm, event.instance_id,
                        constants.acknowledge_event).execute_workflow()

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.acknowledge(updated_next_alert)
            cache_task_instances.store_single_instance(client, inst)

    return updated_next_alert


def resolve(conn, client, event, org_id, has_comp_perm=False, has_team_perm=False, is_sys_action=False,
            lang=constants.lang_en, skip_syncing=None, voice_url=None, from_num=None, to_num=None, org_perm=None):
    '''
    Resolves a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: ResolveEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :param lang: language to send resolution updates in
    :param skip_syncing: (list) of integration types to skip
    :param voice_url: URL of the voice note (only for live call routing incidents)
    :param from_num: caller's number (only for live call routing incidents)
    :param to_num: the live call routing number (only for live call routing incidents)
    :param org_perm: organization permissions
    :return: (timestamp) next alert timestamp
    '''
    to_sync_instance = True if org_perm is not None and permissions.has_org_permission(
        org_perm, permissions.ORG_INTEGRATION_SECONDARY_PERMISSION) else False
    to_send_status_update = True if org_perm is not None and permissions.has_org_permission(
        org_perm, permissions.ORG_INCIDENT_STATUS_PERMISSION) else False

    # Workflows may not run correctly if the incident is already resolved.
    # That is why workflows should be run before resolving the incident.
    if org_perm is not None and permissions.has_org_permission(org_perm, permissions.ORG_WORKFLOWS_PERMISSION):
        WorkflowManager(conn, client, org_id, org_perm, event.instance_id,
                        constants.resolve_event).execute_workflow()

    # We have to get the synced integrations of the instance first because once it is resolved the query
    # will return an empty result as it only looks for open instances.
    synced_integs = []
    if to_sync_instance:
        synced_integs = syncer_services.get_synced_instance_integrations(conn, client, event.event_timestamp,
                                                                         event.instance_id)
    try:
        resolved_instance_ids = db_events.book_resolve_event(conn, event, org_id, has_comp_perm, has_team_perm,
                                                             is_sys_action)
    except LookupError:
        logging.info(errors.err_unknown_resource + ' | Could not resolve Incident #' + str(event.instance_id))
        if settings.CACHE_ON:
            logging.info('Removing from cache: Incident #' + str(event.instance_id))
            cache_task_instances.remove_instance(client, event.instance_id)
        raise

    if resolved_instance_ids is not None and len(resolved_instance_ids) > 0:
        # Handle synced instance actions here
        if to_sync_instance:
            syncer_services.run_integration_syncer(
                conn, client, event.event_timestamp, org_id, event.instance_id, event_by=event.event_by,
                synced_integs=synced_integs, skip_syncing=skip_syncing, new_status=constants.resolved_state,
                voice_url=voice_url, from_num=from_num, to_num=to_num, org_perm=org_perm
            )

        if settings.CACHE_ON:
            cache_task_instances.remove_instance(client, resolved_instance_ids)

            if to_send_status_update:
                store_pending_instance_update(client, event.event_timestamp, org_id, event.instance_id,
                                              constants.resolve_event)

    # If organization is blacklisted, take this resolution as evidence of activity and remove all the blocks.
    syncer_organizations.remove_blacklisted_organization(conn, client, org_id)


def escalate(conn, client, escalations, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Escalates task instances.
    :param conn: db connection
    :param client: cache client
    :param escalations: (list of tuple) -> [(EscalateEvent, new Assignees), ...]
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :return: (dict of InstanceState)  |  None if escalations could not be processed correctly
    '''
    db_events.book_escalate_events(conn, escalations, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst_id_list, event_tmsp_list = [], []
        for item in escalations:
            inst_id_list.append(item[0].instance_id)
            event_tmsp_list.append(item[0].event_timestamp)

        if len(inst_id_list) > 0 and len(event_tmsp_list) > 0:
            inst_dict = db_task_instances.get_instances(conn, max(event_tmsp_list), inst_id_list)
            if len(inst_dict) > 0:
                cache_task_instances.store_multiple_instances(client, list(inst_dict.values()))

            return inst_dict
    return None


def bulk_system_escalate(conn, client, timestamp, escalations):
    '''
    Auto-escalate (system generated) task instances.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param escalations: (list of tuple) -> [(EscalateEvent, new Assignees), ...]
    :return: (dict of list of instance assignee objects)
    '''
    curr_inst_assignees = db_events.book_bulk_system_escalate_events(conn, timestamp, escalations)

    if settings.CACHE_ON:
        if len(curr_inst_assignees) > 0:
            instances_to_upd = cache_task_instances.get_multiple_instances(client, list(curr_inst_assignees.keys()))
            if len(instances_to_upd) > 0:
                for inst_id in instances_to_upd:
                    inst_obj = instances_to_upd[inst_id]
                    inst_obj.assignees = curr_inst_assignees[inst_id]

                cache_task_instances.store_multiple_instances(client, list(instances_to_upd.values()))

    return curr_inst_assignees


def snooze(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Snoozes a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: SnoozeEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_snooze_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.snooze(event)
            cache_task_instances.store_single_instance(client, inst)


def reassign(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Reassigns a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: ReassignEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    new_assignees = db_events.book_reassign_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            pol_objs = syncer_policies.get_policies(conn, client, event.event_timestamp, event.reassign_to)
            next_alert_tmsp = InstanceState.calculate_next_alert_timestamp(
                1, list(pol_objs.values()), event.event_timestamp)
            inst.reassign(event.event_timestamp, new_assignees, next_alert_tmsp)
            cache_task_instances.store_single_instance(client, inst)


def add_responders(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False,
                   notifier=None):
    '''
    Adds new responders to a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: AddRespondersEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :param notifier: NoticeAllocator object
    '''
    new_assignees = db_events.book_add_responders_event(conn, event, org_id, has_comp_perm, has_team_perm,
                                                        is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.add_responders(new_assignees)
            cache_task_instances.store_single_instance(client, inst)

            if inst.conference_bridges is not None:
                disp_info = db_policies.get_policy_dispatch_info(conn, event.event_timestamp, event.new_responders)
                disp_list = []
                for p_id in disp_info:
                    p_item = disp_info[p_id]
                    disp_list.append({
                        var_names.user_id: p_item[var_names.user_id],
                        var_names.email: p_item[var_names.email],
                        var_names.language: p_item[var_names.language],
                        var_names.push_token: p_item[var_names.push_token]
                    })

                    notifier.handle_conference_bridge_dispatch(
                        inst.instance_id, inst.organization_instance_id, inst.task.title(), inst.conference_bridges[0],
                        disp_list, inst.organization_id
                    )
    return notifier


def un_acknowledge(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False,
                   org_perm=None):
    '''
    Un-acknowledges a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: UnAcknowledgeEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :param org_perm: organization permissions
    '''
    db_events.book_un_acknowledge_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    syncer_services.run_integration_syncer(conn, client, event.event_timestamp, org_id, event.instance_id,
                                           event_by=event.event_by, new_status=constants.open_state, org_perm=org_perm)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.un_acknowledge(event.event_timestamp, event.next_alert_timestamp)
            cache_task_instances.store_single_instance(client, inst)


def amend_urgency(conn, client, urgency_events, org_id=None, has_comp_perm=False, has_team_perm=False,
                  is_sys_action=False, org_perm=None):
    '''
    Amend the urgency of a task instance.
    :param conn: db connection
    :param client: cache client
    :param urgency_events: (list) of UrgencyAmendmentEvent(s)
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :param org_perm: organization permissions
    '''
    db_events.book_urgency_amendment_events(conn, urgency_events, org_id, has_comp_perm, has_team_perm, is_sys_action)
    event = urgency_events[0]

    # Handle synced instance actions here
    syncer_services.run_integration_syncer(conn, client, event.event_timestamp, org_id, event.instance_id,
                                           event_by=event.event_by, new_urgency=event.new_urgency, org_perm=org_perm)

    if org_perm is not None and permissions.has_org_permission(org_perm, permissions.ORG_WORKFLOWS_PERMISSION):
        WorkflowManager(conn, client, org_id, org_perm, event.instance_id,
                        constants.urgency_amendment_event).execute_workflow()

    if settings.CACHE_ON:
        inst_id_list = [event.instance_id for event in urgency_events]
        instances = cache_task_instances.get_multiple_instances(client, inst_id_list)

        updated_instances = []
        for event in urgency_events:
            if event.instance_id in instances:
                inst = instances[event.instance_id]
                inst.amend_urgency(event.new_urgency)
                updated_instances.append(inst)

        if len(updated_instances) > 0:
            cache_task_instances.store_multiple_instances(client, updated_instances)


def merge(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Merges a task instance with another.
    :param conn: db connection
    :param client: cache client
    :param event: MergeEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_merge_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        cache_task_instances.remove_instance(client, event.instance_id)


def un_merge(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Un-merge a task instance from another.
    :param conn: db connection
    :param client: cache client
    :param event: MergeEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :return: (integer) -> instance ID of the newly created instance
    '''
    new_inst_id = db_events.book_un_merge_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        # update the instance on which the un-merging is happening
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.un_merge()
            cache_task_instances.store_single_instance(client, inst)

        # store the newly created instance from the split task
        new_inst_obj = db_task_instances.get_instances(conn, event.event_timestamp, new_inst_id)[new_inst_id]
        cache_task_instances.store_single_instance(client, new_inst_obj)

    return new_inst_id


def redact(conn, client, timestamp, instance_id, org_id, user_id):
    '''
    Redact a task instance.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param instance_id: ID of the instance to redact
    :param org_id: ID of the organization the instance belongs to
    :param user_id: ID of the user redacting the instance
    '''
    db_task_instances.redact_instance(conn, timestamp, instance_id, org_id, user_id)
    if settings.CACHE_ON:
        cache_task_instances.remove_instance(client, instance_id)


def add_subscribers(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Add subscribers to a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: AddRespondersEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_add_subscribers_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.add_subscribers(event.subscribers)
            cache_task_instances.store_single_instance(client, inst)


def remove_subscribers(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Remove subscribers from a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: AddRespondersEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_remove_subscribers_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.remove_subscribers(event.subscribers)
            cache_task_instances.store_single_instance(client, inst)


def add_impacted_business_service(conn, client, event, business_service_name, org_id=None, has_comp_perm=False,
                                  has_team_perm=False, is_sys_action=False):
    '''
    Add a business impact to a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: AddRespondersEvent
    :param business_service_name: name of the business service
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_add_impacted_business_service_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.add_impacted_business_service(event.business_service_id, business_service_name)
            cache_task_instances.store_single_instance(client, inst)

        store_pending_instance_update(client, event.event_timestamp, org_id, event.instance_id,
                                      constants.add_impacted_business_service_event)


def remove_impacted_business_service(conn, client, event, org_id=None, has_comp_perm=False,
                                     has_team_perm=False, is_sys_action=False):
    '''
    Remove a business impact from a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: AddRespondersEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_remove_impacted_business_service_event(conn, event, org_id, has_comp_perm, has_team_perm,
                                                          is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.remove_impacted_business_service(event.business_service_id)
            cache_task_instances.store_single_instance(client, inst)

        store_pending_instance_update(client, event.event_timestamp, org_id, event.instance_id,
                                      constants.remove_impacted_business_service_event)


def enable_instance_alerting(conn, client, timestamp, org_id, instance_id, to_enable=True):
    '''
    Enable alerting for an instance.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param org_id: ID f the organization
    :param instance_id: ID of the instance
    :param to_enable: True if instance alerting should be enabled; False otherwise
    '''
    db_task_instances.enable_instance_alerting(conn, timestamp, org_id, instance_id, to_enable)

    if settings.CACHE_ON:
        inst = get_single_instance(conn, client, timestamp, instance_id, store_misses=False)
        if inst is not None:
            inst.task.details[var_names.to_alert] = to_enable
            cache_task_instances.store_single_instance(client, inst)


def execute_custom_action(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False,
                          is_sys_action=False):
    '''
    Snoozes a task instance.
    :param conn: db connection
    :param client: cache client
    :param event: CustomActionEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_custom_action_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None and event.is_synced:
            inst.sync_with_vendor(event.integration_id, event.integration_type_id, event.vendor_id, event.vendor_url)
            cache_task_instances.store_single_instance(client, inst)


def edit_title(conn, client, event, org_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Edit the title of the task an instance is associated to.
    :param conn: db connection
    :param client: cache client
    :param event: EditTitleEvent
    :param org_id: ID of the organization the instance belongs to
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_edit_title_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)

    if settings.CACHE_ON:
        inst = cache_task_instances.get_single_instance(client, event.instance_id)
        if inst is not None:
            inst.edit_title(event.task_title)
            cache_task_instances.store_single_instance(client, inst)


def notate(conn, client, event, org_id=None, org_perm=None, has_comp_perm=False, has_team_perm=False,
           is_sys_action=False):
    '''
    Adds a note to an instance and syncs it up with external integrations.
    :param conn: db connection
    :param client: cache client
    :param event: EditTitleEvent
    :param org_id: ID of the organization the instance belongs to
    :param org_perm: permissions the organization has
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_notate_event(conn, event, org_id, has_comp_perm, has_team_perm, is_sys_action)
    syncer_services.run_integration_syncer(conn, client, event.event_timestamp, org_id, event.instance_id,
                                           event_by=event.event_by, new_note=event.notes, org_perm=org_perm)
    if org_perm is not None and permissions.has_org_permission(org_perm, permissions.ORG_WORKFLOWS_PERMISSION):
        WorkflowManager(conn, client, org_id, org_perm, event.instance_id, constants.notate_event).execute_workflow()


def update_status(conn, client, event, org_id=None, org_perm=None, has_comp_perm=False, has_team_perm=False,
                  is_sys_action=False):
    '''
    Adds a note to an instance and syncs it up with external integrations.
    :param conn: db connection
    :param client: cache client
    :param event: EditTitleEvent
    :param org_id: ID of the organization the instance belongs to
    :param org_perm: permissions the organization has
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    '''
    db_events.book_status_update_event(conn, event, org_id=org_id, has_comp_perm=has_comp_perm,
                                       has_team_perm=has_team_perm, is_sys_action=is_sys_action)

    store_pending_instance_update(
        client, event.event_timestamp, org_id, event.instance_id, constants.status_update_event
    )
    syncer_services.run_integration_syncer(
        conn, client, event.event_timestamp, org_id, event.instance_id,
        event_by=event.event_by, new_status_update=event.update, org_perm=org_perm
    )


def add_conference_bridge(conn, client, timestamp, notifier, org_id, instance_id, conf_details, access_method,
                          user_id=None, has_comp_perm=False, has_team_perm=False, is_sys_action=False):
    '''
    Add conference bridge. There is no cache related transaction here, but this function
    allows the operation to be centralized.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param notifier: NoticeAllocator object
    :param org_id: organization ID
    :param instance_id: ID of the instance the conference bridge is being added to
    :param conf_details: (dict) of conference bridge information ->
        {conference_phone: .., conference_url: .., integration_type: .., external_id: .., external_info: ..}
    :param access_method: the method that was used to interact with the application
    :param user_id: ID of the user who triggered the function
    :param has_comp_perm: (boolean) does the organization have advanced permissions
    :param has_team_perm: (boolean) does the organization have team permissions
    :param is_sys_action: (boolean) is this action being automatically performed by the system or not
    :return: NoticeAllocator object
    '''
    disp_info = db_task_instances.get_instance_assignee_dispatch_info(conn, timestamp, instance_id)
    org_inst_id = disp_info[var_names.organization_instance_id]
    inst_title = disp_info[var_names.task_title]
    assignee_emails = [x[var_names.email] for x in disp_info[var_names.assignees]]

    if conf_details[var_names.integration_type] == intt.microsoft_teams:
        meeting_id, conf_url = microsoft_teams.create_meeting(
            timestamp, conf_details[var_names.additional_info][var_names.external_id],
            conf_details[var_names.additional_info][var_names.user_object_id], inst_title, assignee_emails
        )
        conf_phone = None
        addn_info = {var_names.meeting_id: meeting_id}
    elif conf_details[var_names.integration_type] == intt.webex:
        meeting_id, conf_url = webex.create_meeting(
            conn, timestamp, org_id, conf_details[var_names.external_id],
            conf_details[var_names.external_info][var_names.access_token],
            conf_details[var_names.external_info][var_names.refresh_token],
            inst_title, assignee_emails
        )
        conf_phone = None
        addn_info = {var_names.meeting_id: meeting_id}
    elif conf_details[var_names.integration_type] == intt.zoom:
        meeting_id, conf_url = zoom.create_meeting(
            conn, timestamp, org_id, conf_details[var_names.external_id],
            conf_details[var_names.external_info][var_names.access_token],
            conf_details[var_names.external_info][var_names.refresh_token],
            inst_title, assignee_emails
        )
        conf_phone = None
        addn_info = {var_names.meeting_id: meeting_id}
    else:
        conf_url = conf_details[var_names.conference_url]
        conf_phone = conf_details[var_names.conference_phone]
        addn_info = None

    if conf_url is not None or conf_phone is not None:
        event = AddConferenceBridgeEvent(instance_id, timestamp, access_method, conf_url,
                                         conf_phone, addn_info, user_id)
        db_events.book_add_conference_bridge_event(
            conn, event, org_id=org_id, has_comp_perm=has_comp_perm, has_team_perm=has_team_perm,
            is_sys_action=is_sys_action
        )

        conf_bridge = {
            var_names.conference_url: conf_url,
            var_names.conference_phone: conf_phone
        }

        notifier.handle_conference_bridge_dispatch(
            instance_id, org_inst_id, inst_title, conf_bridge, disp_info[var_names.assignees], org_id
        )

        if settings.CACHE_ON:
            inst = cache_task_instances.get_single_instance(client, event.instance_id)
            if inst is not None:
                inst.conference_bridges = [conf_bridge]
                cache_task_instances.store_single_instance(client, inst)

    return notifier


def store_pending_instance_update(client, timestamp, organization_id, instance_id, event_type):
    '''
    Store details of instance updates that should be sent out.
    :param client: cache client
    :param timestamp: timestamp when the update was made
    :param organization_id: ID of the organization
    :param event_type: type of event (e.g. RESOLVE, STATUS UPDATE, etc)
    :param instance_id: ID of the instance
    '''
    if settings.CACHE_ON:
        update_details = {
            var_names.organization_id: organization_id,
            var_names.instance_id: instance_id,
            var_names.timestamp: timestamp,
            var_names.event_type: event_type
        }
        cache_task_instances.store_pending_instance_update(client, update_details)
