# By: Riasat Ullah
# This file handles the execution of workflows. The conditional logic and execution flow is also handled here.

from data_syncers import syncer_task_instances, syncer_workflows
from dbqueries import db_accounts, db_business_services, db_events, db_instance_utilities, db_integrations, \
    db_task_instances, db_teams
from integrations import slack
from modules.alert_logger import AlertLogger
from modules import custom_action_manager
from modules.notice_allocator import NoticeAllocator
from objects.events import AddImpactedBusinessServiceEvent, AddRespondersEvent, AddSubscribersEvent, NotateEvent, \
    ReassignEvent, ResolveEvent, RunWorkflowEvent, SendChatMessageEvent, SendExternalEmailEvent, SendExternalSmsEvent, \
    StatusUpdateEvent
from objects.routing_rule import RoutingRule
from utils import app_notice, constants, helpers, integration_type_names as intt, logging, mail, permissions, times, \
    var_names
from utils.communication_vendors import Twilio


class WorkflowManager(object):

    def __init__(self, conn, cache_client, organization_id, org_perm, instance_id, instance_trigger_event=None,
                 workflow_ids=None, user_id=None, access_method=constants.internal, lang=constants.lang_en):
        self.conn = conn
        self.cache_client = cache_client
        self.organization_id = organization_id
        self.org_perm = org_perm
        self.instance_id = instance_id
        self.instance_trigger_event = instance_trigger_event

        # switch to syncer
        self.workflows = syncer_workflows.get_organization_enabled_workflows(
            self.conn, self.cache_client, times.get_current_timestamp(), self.organization_id)
        if workflow_ids is not None and len(self.workflows) > 0:
            self.workflows = [x for x in self.workflows if x[var_names.workflow_id] in workflow_ids]

        self.instance = db_task_instances.get_instance_for_workflow(
            self.conn, times.get_current_timestamp(), self.instance_id) if len(self.workflows) > 0 else None
        self.user_id = user_id
        self.access_method = access_method
        self.lang = lang

        # This will store the http status and response of every custom action.
        self.integ_details = []
        self.action_outcomes = []

    def execute_workflow(self):
        if len(self.workflows) > 0 and self.instance is not None:
            for flow in self.workflows:
                # Clean up action outcomes and start from scratch for every workflow
                self.action_outcomes = []
                if self.trigger_qualifies(flow):
                    event = RunWorkflowEvent(self.instance_id, times.get_current_timestamp(), self.access_method,
                                             flow[var_names.workflow_id], flow[var_names.workflow_name],
                                             event_by=self.user_id)
                    db_events.book_run_workflow_event(self.conn, event)

                    for step in flow[var_names.workflow]:
                        self.run_workflow_step(flow[var_names.workflow_id], step)

    def trigger_qualifies(self, workflow):
        '''
        Check if the workflow is eligible to be run. When the workflow is being run manually, ensure that
        it has manual run permission. If it is being run automatically, ensure that it can be run after the
        specified trigger event.
        :param workflow: (dict) workflow item
        :return: (boolean) True if the workflow can be run; False otherwise
        '''
        if self.instance_trigger_event is None:
            if workflow[var_names.is_manual]:
                return True
        else:
            if workflow[var_names.trigger_method] is not None and\
                    self.instance_trigger_event in workflow[var_names.trigger_method]:
                return True
        return False

    def run_workflow_step(self, workflow_id, step):
        '''
        Run a workflow step.
        :param workflow_id: ID of the workflow (will be needed for the has workflow already been run condition)
        :param step: (dict) single workflow step
        '''
        if var_names.actions in step:
            self.execute_action(step)
        else:
            if var_names.block_if in step:
                if self.all_group_conditions_qualify(workflow_id, step[var_names.block_if][var_names.conditions]):
                    for item in step[var_names.block_if][var_names.workflow]:
                        self.run_workflow_step(workflow_id, item)

                else:
                    if var_names.block_else in step and len(step[var_names.block_else]) > 0:
                        for item in step[var_names.block_else]:
                            self.run_workflow_step(workflow_id, item)

    def all_group_conditions_qualify(self, workflow_id, group_conditions_list):
        '''
        Check if all group conditions in a workflow step qualify or not.
        :param workflow_id: workflow ID
        :param group_conditions_list: (list) of group conditions
        :return: (boolean) True or False
        '''
        is_true = True
        i = 0
        while i < len(group_conditions_list):
            grp_logic = list(group_conditions_list[i].keys())[0]
            grp_conds = group_conditions_list[i][grp_logic]
            is_grp_cond_true = self.group_condition_qualifies(workflow_id, grp_conds)
            if grp_logic == "and":
                is_true = is_true and is_grp_cond_true
            else:
                is_true = is_true or is_grp_cond_true
            i += 1
        return is_true

    def group_condition_qualifies(self, workflow_id, group_conditions):
        '''
        Check if a single group condition qualifies or not.
        :param workflow_id: workflow ID
        :param group_conditions: (list) of conditions
        :return: (boolean) True or False
        '''
        is_true = True
        i = 0
        while i < len(group_conditions):
            cond = group_conditions[i]
            is_cond_true = self.condition_qualifies(workflow_id, cond)
            if cond[var_names.logic] == "and":
                is_true = is_true and is_cond_true
            else:
                is_true = is_true or is_cond_true
            i += 1
        return is_true

    def condition_qualifies(self, workflow_id, condition):
        '''
        Checks if a condition qualifies or not.
        :param workflow_id: workflow ID
        :param condition: (dict) condition item
        :return: (boolean) True or False
        '''
        rule = RoutingRule(condition[var_names.rule_type], condition[var_names.field_name],
                           condition[var_names.comparator], condition[var_names.field_value])
        try:
            if rule.rule_type == var_names.workflow_not_run:
                if self.instance.was_workflow_run(workflow_id):
                    return False
                else:
                    return True

            elif rule.rule_type == var_names.service:
                given_value = self.instance.task.service_id()

            elif rule.rule_type == var_names.status:
                given_value = self.instance.status

            elif rule.rule_type == var_names.urgency_level:
                given_value = self.instance.task.urgency_level()

            elif rule.rule_type == var_names.email_from and\
                var_names.source_payload in self.instance.task.details[var_names.source_payload] and\
                    var_names.email_from in\
                    self.instance.task.details[var_names.source_payload][var_names.source_payload]:

                given_value = self.instance.task.details[var_names.source_payload][
                    var_names.source_payload][var_names.email_from]

            elif rule.rule_type == var_names.email_to and\
                var_names.source_payload in self.instance.task.details[var_names.source_payload] and\
                    var_names.email_to in\
                    self.instance.task.details[var_names.source_payload][var_names.source_payload]:

                given_value = self.instance.task.details[var_names.source_payload][
                    var_names.source_payload][var_names.email_to]

            elif rule.rule_type == var_names.previous_action_http_status:
                if len(self.action_outcomes) == 0:
                    return False
                given_value = self.action_outcomes[-1][1]

            elif rule.rule_type == var_names.previous_action_json:
                if len(self.action_outcomes) == 0:
                    return False
                given_value = str(helpers.get_dict_value_from_dotted_key(self.action_outcomes[-1][2], rule.field_name))
            else:
                given_value = str(helpers.get_dict_value_from_dotted_key(
                    self.instance.task.details[var_names.source_payload], rule.field_name))

            # Service ref id in the displayable_payload is concealed, and is stored as such in the database as well.
            # The given value above is also concealed. Hence, no unmasking is required.

            # "exists" comparator is automatically validated if given value is found
            if rule.comparator == constants.rule_exists:
                return True
            elif rule.comparator == constants.rule_equals:
                return rule.equals(given_value)
            elif rule.comparator == constants.rule_contains:
                return rule.contains(given_value)
            elif rule.comparator == constants.rule_matches:
                return rule.matches(given_value)
            elif rule.comparator == constants.rule_not_equals:
                return not rule.equals(given_value)
            elif rule.comparator == constants.rule_not_contains:
                return not rule.contains(given_value)
            elif rule.comparator == constants.rule_not_matches:
                return not rule.matches(given_value)
            else:
                logging.error('Unknown comparator provided. Automatically disqualifying rule - ' + rule.comparator)
                return False
        except (KeyError, TypeError):
            # Only validate "not_exists" when there is a KeyError
            if rule.comparator == constants.rule_not_exists:
                return True
            else:
                return False

    def execute_action(self, actions):
        '''
        Execute a workflow action.
        :param actions: (dict) -> {actions: {...}}
        '''
        current_time = times.get_current_timestamp()
        actions = actions[var_names.actions]

        # New responders
        if var_names.new_responders in actions:
            if permissions.has_org_permission(self.org_perm, permissions.ORG_ADD_RESPONDERS_PERMISSION):
                event = AddRespondersEvent(self.instance_id, current_time, constants.internal,
                                           [int(x) for x in actions[var_names.new_responders]])
                notifier = NoticeAllocator()
                notifier = syncer_task_instances.add_responders(
                    self.conn, self.cache_client, event, org_id=self.organization_id, is_sys_action=True,
                    notifier=notifier
                )
                self.dispatch_notices(notifier)

        # User subscribers
        elif var_names.user_subscribers in actions:
            if permissions.has_org_permission(self.org_perm, permissions.ORG_INCIDENT_STATUS_PERMISSION):
                subscriber_ids = [int(x) for x in actions[var_names.user_subscribers]]
                event = AddSubscribersEvent(self.instance_id, current_time, constants.internal, subscriber_ids)
                syncer_task_instances.add_subscribers(self.conn, self.cache_client, event, org_id=self.organization_id,
                                                      is_sys_action=True)

        # Team subscribers
        elif var_names.team_subscribers in actions:
            if permissions.has_org_permission(self.org_perm, permissions.ORG_INCIDENT_STATUS_PERMISSION):
                subscriber_ids = db_teams.get_team_member_ids(
                    self.conn, current_time, [int(x) for x in actions[var_names.team_subscribers]])
                if len(subscriber_ids) > 0:
                    event = AddSubscribersEvent(self.instance_id, current_time, constants.internal, subscriber_ids)
                    syncer_task_instances.add_subscribers(self.conn, self.cache_client, event,
                                                          org_id=self.organization_id, is_sys_action=True)

        # Conference bridge
        elif var_names.conference_bridge in actions:
            if permissions.has_org_permission(self.org_perm, permissions.ORG_CONFERENCE_BRIDGES_PERMISSION):
                conf_details = db_instance_utilities.get_conference_bridge_details(
                    self.conn, current_time, int(actions[var_names.conference_bridge]))
                notifier = NoticeAllocator()
                notifier = syncer_task_instances.add_conference_bridge(
                    self.conn, self.cache_client, current_time, notifier, self.organization_id, self.instance_id,
                    conf_details, constants.internal, is_sys_action=True
                )
                self.dispatch_notices(notifier)

        # Status update
        elif var_names.status_update in actions:
            if permissions.has_org_permission(self.org_perm, permissions.ORG_INCIDENT_STATUS_PERMISSION):
                event = StatusUpdateEvent(self.instance_id, current_time, constants.internal,
                                          self.extract_message(actions[var_names.status_update]))
                syncer_task_instances.update_status(
                    self.conn, self.cache_client, event, self.organization_id,
                    org_perm=self.org_perm, is_sys_action=True
                )

        # Notes
        elif var_names.notes in actions:
            if self.instance_trigger_event != constants.notate_event:
                event = NotateEvent(self.instance_id, current_time, constants.internal,
                                    None, self.extract_message(actions[var_names.notes]))
                syncer_task_instances.notate(self.conn, self.cache_client, event, self.organization_id,
                                             org_perm=self.org_perm, is_sys_action=True)

        # Business impact
        elif var_names.business_impact in actions:
            bus_map = db_business_services.get_business_service_names(self.conn, current_time, self.organization_id)
            for bus_id in actions[var_names.business_impact]:
                event = AddImpactedBusinessServiceEvent(self.instance_id, current_time, constants.internal, bus_id)
                syncer_task_instances.add_impacted_business_service(
                    self.conn, self.cache_client, event, bus_map[bus_id], org_id=self.organization_id,
                    is_sys_action=True
                )

        # Reassign
        elif var_names.reassign_to in actions:
            event = ReassignEvent(self.instance_id, current_time, constants.internal, None,
                                  [int(x) for x in actions[var_names.reassign_to]])
            syncer_task_instances.reassign(self.conn, self.cache_client, event, org_id=self.organization_id,
                                           is_sys_action=True)

        # Resolve
        elif var_names.to_resolve in actions:
            if self.instance_trigger_event != constants.resolve_event:
                event = ResolveEvent(self.instance_id, current_time, constants.internal)
                syncer_task_instances.resolve(
                    self.conn, self.cache_client, event, org_id=self.organization_id, is_sys_action=True,
                    org_perm=self.org_perm
                )

        # Send an email
        elif var_names.email in actions:
            email_to = actions[var_names.email]
            email_subj = self.extract_message(actions[var_names.email_subject])
            email_msg = self.extract_message(actions[var_names.text_msg])
            email_credentials = mail.AmazonSesCredentials(constants.notifier_email_account)

            mail.AmazonSesDispatcher(email_subj, email_msg, email_to, email_credentials).start()
            event = SendExternalEmailEvent(self.instance_id, current_time, constants.internal, email_to, None)
            db_events.book_send_external_email_event(self.conn, event, self.organization_id, is_sys_action=True)

        elif var_names.send_sms in actions:
            sms_to = actions[var_names.send_sms]
            sms_msg = self.extract_message(actions[var_names.text_msg])

            vendor_numbers = db_accounts.get_vendor_phones(self.conn, current_time)
            twilio_client = Twilio(Twilio.get_authentication())
            final_recipients = dict()
            for send_to in sms_to:
                send_to = self.extract_message(send_to)
                phone_iso = twilio_client.get_phone_iso_code(send_to)
                if phone_iso is not None:
                    vendor_details = vendor_numbers[(phone_iso, var_names.text_allowed)]
                    send_from = vendor_details[var_names.phone]
                    sid = twilio_client.send_sms(send_from, send_to, sms_msg)
                    if sid is not None:
                        if phone_iso not in final_recipients:
                            final_recipients[phone_iso] = []
                        final_recipients[phone_iso].append(send_to)

            event = SendExternalSmsEvent(self.instance_id, current_time, constants.internal, final_recipients, None)
            db_events.book_send_external_sms_event(self.conn, event, self.organization_id, is_sys_action=True)

        elif var_names.send_chat_message in actions:
            integration_id = actions[var_names.send_chat_message]
            chat_msg = self.extract_message(actions[var_names.text_msg])

            if len(self.integ_details) == 0:
                self.integ_details = db_integrations.get_comprehensive_integration_details(
                    self.conn, current_time, self.organization_id)

            integ_item = None
            for item in self.integ_details:
                if item[var_names.integration_id] == integration_id:
                    integ_item = item
                    break

            if integ_item is not None:
                if integ_item[var_names.integration_type] == intt.slack:
                    slack.send_slack_message(integ_item[var_names.vendor_endpoint], chat_msg)

                    event = SendChatMessageEvent(self.instance_id, current_time, constants.internal, integration_id,
                                                 integ_item[var_names.integration_type_id], None)
                    db_events.book_send_chat_message_event(self.conn, event, self.organization_id, is_sys_action=True)

        elif var_names.integration_id in actions:
            if len(self.integ_details) == 0:
                self.integ_details = db_integrations.get_comprehensive_integration_details(
                    self.conn, current_time, self.organization_id)

            integ_item, integ_key = None, None
            for item in self.integ_details:
                if item[var_names.integration_id] == actions[var_names.integration_id]:
                    integ_item = item
                    integ_key = item[var_names.integration_key]
                    break

            if integ_item is not None:
                exe_status, exe_output = custom_action_manager.execute_custom_action(
                    self.conn, self.cache_client, current_time, self.organization_id, self.org_perm, self.instance_id,
                    self.instance, integ_item, integ_key, run_manually=True, lang=self.lang, is_sys_action=True
                )
                exe_name = integ_item[var_names.additional_info][var_names.configuration_name]\
                    if var_names.configuration_name in integ_item[var_names.additional_info]\
                    else integ_item[var_names.integration_name]
                self.action_outcomes.append([exe_name, exe_status, exe_output])

    def dispatch_notices(self, notifier):
        '''
        Dispatch email and push notifications.
        :param notifier: NoticeAllocator object
        '''
        if len(notifier.email_messages) > 0:
            mail.AmazonSesBulkDispatcher(notifier.email_messages).start()

        if len(notifier.push_notices) > 0:
            app_notice.NoticeSender(notifier.push_notices).start()

        if len(notifier.alert_logs) > 0:
            AlertLogger(self.conn, notifier.alert_logs).start()

    def extract_message(self, msg):
        '''
        Extract field parameter values (internal and from responses) and placeholder texts with them.
        :param msg: (str) text with placeholders
        :return: (str) updated text
        '''
        msg = helpers.replace_internal_data_fields(msg, self.instance, self.lang)
        msg = helpers.replace_response_data_fields(msg, self.action_outcomes)
        return msg
