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

from cache_queries import cache_api_keys, cache_email_assignees, cache_integration_keys, cache_organizations,\
    cache_policies, cache_routing, cache_services, cache_task_instances, cache_users
from data_syncers import syncer_routing
from dbqueries import db_accounts, db_members, db_organizations, db_routing, db_services, db_users, db_workflows
from notices import account_notices
from taskcallrest import settings
from utils import constants, helpers, logging, mail, permissions, times, var_names
from validations import string_validator
import configuration
import datetime


def store_organization_permissions_in_cache(conn, client, timestamp):
    '''
    Retrieves permissions of all organizations and stores them in cache.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    '''
    if settings.CACHE_ON:
        org_perms = db_organizations.get_organization_permissions(conn, timestamp)
        cache_organizations.remove_all_organization_permissions(client)
        if len(org_perms) > 0:
            cache_organizations.store_organization_permissions(client, org_perms)


def get_organization_permissions(conn, client, timestamp, org_ids, store_misses=True):
    '''
    Gets the permissions organizations have given a list of organization IDs. Looks in cache first;
    if there are cache misses, then it fetches from the db.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param org_ids: (list) of organization IDs
    :param store_misses: (boolean) True if cache misses retrieved from the db should be stored in cache; False otherwise
    :return: (dict) -> {org id: perm, ...}
    '''
    retrieved_org_perms = dict()
    cache_miss_org_ids = []

    if settings.CACHE_ON:
        retrieved_org_perms = cache_organizations.get_organization_permissions(client, org_ids)
        if len(retrieved_org_perms) != len(org_ids):
            cache_miss_org_ids = list(set(org_ids).difference(set(retrieved_org_perms.keys())))
    else:
        cache_miss_org_ids = org_ids

    if len(cache_miss_org_ids) > 0:
        org_perms_from_db = db_organizations.get_organization_permissions(conn, timestamp, cache_miss_org_ids)
        retrieved_org_perms = {**retrieved_org_perms, **org_perms_from_db}

        if store_misses and settings.CACHE_ON:
            cache_organizations.store_organization_permissions(client, org_perms_from_db)

    return retrieved_org_perms


def get_single_organization_permission(conn, client, timestamp, org_id, store_misses=True):
    '''
    Gets the permissions of an organization. Looks in cache first; if it is not found, then fetches from the db.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param org_id: organization ID
    :param store_misses: (boolean) True if cache misses retrieved from the db should be stored in cache; False otherwise
    :return: (str) -> org perm
    '''
    org_perm = None

    if settings.CACHE_ON:
        org_perm = cache_organizations.get_single_organization_permission(client, org_id)

    if org_perm is None:
        org_perm = list(db_organizations.get_organization_permissions(conn, timestamp, org_id).values())[0]

        if store_misses and settings.CACHE_ON and org_perm is not None:
            cache_organizations.store_single_organization_permissions(client, org_id, org_perm)

    return org_perm


def refresh_organization_month_text_call_costs(conn, client):
    '''
    Retrieves the percentage of total costs incurred by all organizations from the db and stores them in cache.
    :param conn: db connection
    :param client: cache client
    '''
    if settings.CACHE_ON:
        cached_org_ids = cache_organizations.get_organization_ids_from_cached_month_costs(client)
        if len(cached_org_ids) > 0:
            new_org_costs = db_accounts.get_month_text_call_costs(conn, cached_org_ids)
            cache_organizations.store_organization_month_costs(client, new_org_costs)


def get_organization_month_text_call_costs(conn, client, org_ids, store_misses=True):
    '''
    Get the monthly text and call costs
    :param conn: db connection
    :param client: cache client
    :param org_ids: (list of int) of organization IDs
    :param store_misses: (boolean) True if cache misses should be stored in cache after retrieving from db
    :return: (dict) -> {org ID: cost percentage, ...}
    '''
    retrieved_org_costs = dict()
    cache_miss_org_ids = []

    if settings.CACHE_ON:
        retrieved_org_costs = cache_organizations.get_organization_month_costs(client, org_ids)
        if len(retrieved_org_costs) != len(org_ids):
            cache_miss_org_ids = list(set(org_ids).difference(set(retrieved_org_costs.keys())))
    else:
        cache_miss_org_ids = org_ids

    if len(cache_miss_org_ids) > 0:
        org_costs_from_db = db_accounts.get_month_text_call_costs(conn, cache_miss_org_ids)
        retrieved_org_costs = {**retrieved_org_costs, **org_costs_from_db}

        if store_misses and settings.CACHE_ON and len(org_costs_from_db) > 0:
            cache_organizations.store_organization_month_costs(client, org_costs_from_db)

    return retrieved_org_costs


def delete_standard_member(conn, client, timestamp, org_id, member_id, replacing_user_id):
    '''
    Deletes a standard member (not stakeholder) from an organization and syncs up open instances accordingly.
    It also removes the member's policy info from cache.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp this request is being made on
    :param org_id: organization ID of the member
    :param member_id: user_id of the member to be deleted
    :param replacing_user_id: user_id of the member who will take on the role of the member
    :errors: AssertionError, DatabaseError, Redis errors
    '''
    user_ids = db_users.get_user_ids_and_taskcall_email(conn, timestamp, org_id, [member_id, replacing_user_id])

    member_pid, member_rid, member_taskcall_email = user_ids[member_id]
    replacing_pid, replacing_rid, replacing_taskcall_email = user_ids[replacing_user_id]
    new_inst_assignments = [{var_names.user_policyid: replacing_pid, var_names.for_policyid: replacing_pid}]

    # check to see if any conditional routing is set to re-route to the user's policy and update it
    switched_routings = db_routing.get_component_removed_routing_actions(conn, timestamp, org_id, member_pid,
                                                                         replacing_pid)
    if len(switched_routings) == 0:
        switched_routings = None

    # check to see if any workflow has the user as subscriber or responder
    switched_workflows = db_workflows.check_component_in_workflow(
        conn, timestamp, org_id, pol_id=member_pid, replacing_pol_id=replacing_pid,
        user_id=member_id, replacing_user_id=replacing_user_id
    )
    if len(switched_workflows) == 0:
        switched_workflows = None

    # delete the user and hand over responsibilities to the replacing user
    inst_level_assignments = db_members.delete_standard_member(conn, timestamp, org_id, member_id, member_pid,
                                                               member_rid, replacing_user_id, replacing_pid,
                                                               replacing_rid, new_inst_assignments,
                                                               updated_route_actions=switched_routings,
                                                               updated_workflows=switched_workflows)

    if settings.CACHE_ON and inst_level_assignments is not None:
        inst_ids = [x[var_names.instance_id] for x in inst_level_assignments]
        editable_instances = cache_task_instances.get_multiple_instances(client, inst_ids)
        for id_ in editable_instances:
            inst = editable_instances[id_]
            for assignee_ in inst.assignees:
                if assignee_.policy_id == member_pid:
                    assignee_.for_policy_id = replacing_pid
                    assignee_.policy_id = replacing_pid

        if len(editable_instances) > 0:
            cache_task_instances.store_multiple_instances(client, list(editable_instances.values()))

        cache_policies.remove_policies(client, [member_pid])

        if member_taskcall_email is not None:
            cache_email_assignees.remove_email_assignees(client, member_taskcall_email)

        if switched_routings is not None:
            syncer_routing.update_organization_conditional_routes_in_cache(conn, client, timestamp, org_id)


def update_subdomain(conn, client, timestamp, org_id, subdomain):
    '''
    Updates an organization's subdomain.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp this request is being made on
    :param org_id: ID of the organization
    :param subdomain: the new subdomain
    :errors: AssertionError, DatabaseError, ValueError, Redis errors
    '''
    subdomain = subdomain.lower()
    standard_members = db_users.get_users_with_assigned_emails(conn, timestamp, org_id)
    editable_services = db_services.get_email_integrated_service_basic_info(conn, timestamp, org_id)

    # these two variables are for the cache
    del_assignees = []
    new_assignees = dict()

    user_values = []
    for item in standard_members:
        new_email = helpers.construct_taskcall_email_address(item[1].split('@')[0], subdomain)
        user_values.append({
            var_names.user_id: item[0],
            var_names.policy_id: item[2],
            var_names.taskcall_email: new_email
        })
        del_assignees.append(item[1])
        new_assignees[new_email] = [org_id, item[2], None]

    service_values = []
    for item in editable_services:
        assert string_validator.is_email_address(item[3])
        integ_email = helpers.construct_taskcall_email_address(item[3].split('@')[0], subdomain)

        service_values.append({
            var_names.service_id: item[0],
            var_names.integration_id: item[2],
            var_names.integration_email: integ_email,
            var_names.policy_id: item[1]
        })
        del_assignees.append(item[3])
        new_assignees[integ_email] = [org_id, item[1], item[0]]

    db_organizations.update_subdomain(conn, timestamp, org_id, subdomain, user_values, service_values)

    if settings.CACHE_ON:
        cache_email_assignees.remove_email_assignees(client, del_assignees)
        cache_email_assignees.store_email_assignees(client, new_assignees)


def store_all_blacklisted_events_in_cache(conn, client, min_timestamp):
    '''
    Store the blacklisted events caused by all organizations in the cache.
    :param conn: db connection
    :param client: cache client
    :param min_timestamp: the minimum timestamp to fetch the events from
    :errors: AssertionError, DatabaseError, Redis Errors
    '''
    if settings.CACHE_ON:
        blacklisted_events = db_organizations.get_blacklisted_events(conn, min_timestamp)
        cache_organizations.remove_all_blacklisted_events(client)
        if len(blacklisted_events) > 0:
            cache_organizations.store_all_blacklisted_events(client, blacklisted_events)


def store_blacklisted_event(conn, client, timestamp, organization_id, reason, inst_id):
    '''
    Store a potentially malicious or ill-intended events caused by an organization.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp of when the event happened
    :param organization_id: ID of the organization
    :param reason: the reason why this was detected as a threat
    :param inst_id: ID of the instance the blacklisting event is for
    :errors: AssertionError, DatabaseError, Redis Errors
    '''
    db_organizations.blacklist_event(conn, timestamp, organization_id, reason, inst_id)
    if settings.CACHE_ON:
        cache_organizations.store_single_blacklist_event(client, organization_id, timestamp, reason, inst_id)


def is_instance_blacklisted(conn, client, timestamp, org_id, inst_id):
    '''
    Checks if an instance is blacklisted or not.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this check is being requested
    :param org_id: ID of the organization to check for
    :param inst_id: ID of the instance
    :return: (boolean) True if it is blacklisted; False otherwise
    :errors: AssertionError, DatabaseError, Redis Errors
    '''
    min_check_time = timestamp - datetime.timedelta(days=configuration.blacklist_look_back_period)
    if settings.CACHE_ON:
        org_blacklist_items = cache_organizations.get_organization_blacklisted_events(client, org_id)
    else:
        org_bl_dict = db_organizations.get_blacklisted_events(conn, min_check_time, organization_id=org_id)
        org_blacklist_items = org_bl_dict[org_id] if org_id in org_bl_dict else []

    for item in org_blacklist_items:
        if item[var_names.instance_id] == inst_id:
            return True
    return False


def is_organization_blacklisted(conn, client, timestamp, org_id, to_send_notification=True):
    '''
    Checks if an organization is blacklisted or not.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this check is being requested
    :param org_id: ID of the organization to check for
    :param to_send_notification: (boolean) True if a notification should be sent to the owner of the organization
                                warning him that the account has been blacklisted
    :return: (boolean) True if it is blacklisted; False otherwise
    :errors: AssertionError, DatabaseError, Redis Errors
    '''
    min_check_time = timestamp - datetime.timedelta(days=configuration.blacklist_look_back_period)
    if settings.CACHE_ON:
        org_blacklist_items = cache_organizations.get_organization_blacklisted_events(client, org_id)
    else:
        org_bl_dict = db_organizations.get_blacklisted_events(conn, min_check_time, organization_id=org_id)
        org_blacklist_items = org_bl_dict[org_id] if org_id in org_bl_dict else []

    if len(org_blacklist_items) == 0:
        return False
    else:
        mal_events_in_observation_period = sum([1 if times.get_timestamp_from_string(
            x[var_names.timestamp]) >= min_check_time else 0 for x in org_blacklist_items])
        if mal_events_in_observation_period > configuration.max_malicious_events:

            # Label the organization as blacklisted only if it does not have a paid plan.
            # Checking the redaction permission is a way to ensure that there is a paid plan since redaction
            # is allowed for all plans except for the Free plan.
            org_perm = get_single_organization_permission(conn, client, timestamp, org_id)
            has_paid_plan = permissions.has_org_paid_plan(org_perm)
            if (has_paid_plan and mal_events_in_observation_period > configuration.max_malicious_events_paid_plans)\
                    or not has_paid_plan:

                if has_paid_plan and to_send_notification:
                    last_problem_time = max(
                        [times.get_timestamp_from_string(x[var_names.timestamp]) for x in org_blacklist_items])
                    if (timestamp - last_problem_time).days <= configuration.blacklist_notification_period:
                        try:
                            logging.info('Notifying owner: organization blacklisted - ' + str(org_id))
                            owner_info = db_accounts.get_organization_owner_info_for_account_notifications(
                                conn, timestamp, org_id)
                            if len(owner_info) > 0:
                                email_credentials = mail.AmazonSesCredentials(constants.team_email_account)
                                subject, content = account_notices.account_blacklisted_email_content(
                                    owner_info[var_names.language], owner_info[var_names.first_name],
                                    owner_info[var_names.organization_name],
                                )
                                mail.AmazonSesDispatcher(
                                    subject, content, owner_info[var_names.email], email_credentials).start()
                        except Exception:
                            logging.warning('Failed to send owner blacklisted account notification - ' + str(org_id))

                return True

        return False


def remove_blacklisted_organization(conn, client, org_id):
    '''
    Remove a blacklisted organization from cache and database.
    :param conn: db connection
    :param client: cache client
    :param org_id: ID of the organization to check for
    :errors: AssertionError, DatabaseError, Redis Errors
    '''
    if settings.CACHE_ON:
        cache_organizations.remove_single_blacklisted_organization(client, org_id)
    db_organizations.set_blacklisted_events_to_ignore(conn, org_id)


def close_organization(conn, client, timestamp, org_id):
    '''
    Close an organization account.
    :param conn: db connection
    :param client: cache client
    :param timestamp: timestamp when this request is being made
    :param org_id: the organization ID
    :errors: AssertionError, DatabaseError, Redis errors
    '''
    # Get the cache-able keys first before end-dating the organization in the db
    api_keys, tc_emails, integ_keys, pol_ids, serv_ids, inst_ids =\
        db_organizations.get_cacheable_keys_of_organization(conn, timestamp, org_id)
    db_organizations.close_organization(conn, timestamp, org_id)

    if settings.CACHE_ON and client is not None:
        # handling the ones keyed on org ID first
        cache_organizations.remove_single_organization_permission(client, org_id)
        cache_organizations.remove_organization_month_costs(client, [org_id])
        cache_organizations.remove_single_blacklisted_organization(client, org_id)
        cache_routing.remove_org_conditional_routes(client, org_id)

        if api_keys is not None and len(api_keys) > 0:
            for item in api_keys:
                cache_api_keys.remove_single_api_key(client, item)

        if tc_emails is not None and len(tc_emails) > 0:
            cache_email_assignees.remove_email_assignees(client, tc_emails)

        if integ_keys is not None and len(integ_keys) > 0:
            cache_integration_keys.remove_integration_keys(client, integ_keys)

        if pol_ids is not None and len(pol_ids) > 0:
            cache_policies.remove_policies(client, pol_ids)
            cache_users.remove_user_policy_info(client, pol_ids)

        if serv_ids is not None and len(serv_ids) > 0:
            cache_services.remove_services(client, serv_ids)

        if inst_ids is not None and len(inst_ids) > 0:
            for item in inst_ids:
                cache_task_instances.remove_instance(client, item)
