# By: Riasat Ullah
# This file contains functions to handle api key related information.

from exceptions.user_exceptions import NotUniqueValue
from utils import constants, errors, permissions, var_names
from validations import string_validator
import configuration as configs
import datetime
import psycopg2


def create_api_key(conn, timestamp, user_id, organization_id, api_key, key_name, access_type, api_version,
                   for_user_id=None, ip_restrictions=None):
    '''
    Create an API key; both global and user specific keys can be created with this function.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param user_id: user_id of the user creating the api key
    :param organization_id: ID of the organization this key belongs to
    :param api_key: the actual API key
    :param key_name: readable name to identify the API key with
    :param access_type: the type of access this API key will have
    :param api_version: the version/type of API this key will have access to
    :param for_user_id: user_id of the user who the key is for if it is user specific
    :param ip_restrictions: (list) of IP addresses to restrict requests from
    :errors: AssertionError, DatabaseError
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(user_id, int)
    assert isinstance(organization_id, int)
    assert string_validator.is_api_key(api_key)
    assert string_validator.is_standard_name(key_name)
    assert access_type == constants.api_full_access
    assert api_version in configs.allowed_api_versions

    if not is_api_key_name_unique(conn, timestamp, organization_id, key_name):
        raise NotUniqueValue(errors.err_api_key_name_exists)

    if for_user_id is not None:
        assert isinstance(for_user_id, int)
    if ip_restrictions is not None:
        assert isinstance(ip_restrictions, list)
        for ip in ip_restrictions:
            assert string_validator.is_valid_ip_address(ip)

    key_perm = permissions.create_api_key_permission(api_version)

    query = '''
            insert into api_keys (
                api_key_id, api_key, start_timestamp, end_timestamp, organization_id,
                key_name, created_by, access_type, permissions,
                is_enabled, api_version, for_user_id, ip_restrictions
            ) values (
                nextval('api_keys_seq'), %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s, %s
            )
            '''
    query_params = (api_key, timestamp, constants.end_timestamp, organization_id,
                    key_name, user_id, access_type, key_perm,
                    True, api_version, for_user_id, ip_restrictions,)
    try:
        conn.execute(query, query_params)
    except psycopg2.DatabaseError:
        raise


def edit_api_key(conn, timestamp, organization_id, api_key_id, to_enable=None, ip_restrictions=None):
    '''
    Edits basic details of an existing API key.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param organization_id: ID of the organization the API key belongs to
    :param api_key_id: integer ID of the api key
    :param to_enable: True if should be enabled; False otherwise
    :param ip_restrictions: edited IP restrictions
    :return: the actual API key
    :errors: AssertionError, DatabaseError
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(organization_id, int)
    assert isinstance(api_key_id, int)
    assert to_enable is not None or ip_restrictions is not None

    query_params = {'timestamp': timestamp, 'org_id': organization_id, 'key_id': api_key_id}

    conditions = []
    if to_enable is not None:
        assert isinstance(to_enable, bool)
        conditions.append(" is_enabled = %(is_enbl)s ")
        query_params['is_enbl'] = to_enable

    if ip_restrictions is not None:
        assert isinstance(ip_restrictions, list)
        for ip in ip_restrictions:
            assert string_validator.is_valid_ip_address(ip)
        conditions.append(" ip_restrictions = %(ip_res)s ")
        query_params['ip_res'] = ip_restrictions

    query = '''
            update api_keys set {0}
            where start_timestamp <= %(timestamp)s
                and end_timestamp > %(timestamp)s
                and organization_id = %(org_id)s
                and api_key_id = %(key_id)s
            returning api_key;
            '''.format(','.join(conditions))
    try:
        result = conn.fetch(query, query_params)
        return result[0][0]
    except psycopg2.DatabaseError:
        raise


def delete_api_key(conn, timestamp, organization_id, api_key_id):
    '''
    Delete an API key.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made.
    :param organization_id: ID of the organization the API key belongs to
    :param api_key_id: ID of the API key to delete
    :return: the actual API key
    :errors: AssertionError, DatabaseError
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(organization_id, int)
    assert isinstance(api_key_id, int)

    query = '''
            update api_keys set end_timestamp = %(timestamp)s
            where start_timestamp <= %(timestamp)s
                and end_timestamp > %(timestamp)s
                and organization_id = %(org_id)s
                and api_key_id = %(key_id)s
            returning api_key;
            '''
    query_params = {'timestamp': timestamp, 'org_id': organization_id, 'key_id': api_key_id}
    try:
        result = conn.fetch(query, query_params)
        return result[0][0]
    except psycopg2.DatabaseError:
        raise


def get_api_keys(conn, timestamp, organization_id, for_user_id=None, only_global=True, api_key_id=None):
    '''
    Get API keys given certain parameters.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param organization_id: organization ID to filter by
    :param for_user_id: user_id to filter by for user specific api keys
    :param only_global: True if only the global API keys are wanted
    :param api_key_id: the ID of the API key
    :return: (list) of dict
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(organization_id, int)
    query_params = {'timestamp': timestamp, 'org_id': organization_id}

    conditions = []
    if for_user_id is not None:
        assert isinstance(for_user_id, int)
        conditions.append(" ak.for_user_id = %(user_name)s ")
        query_params['user_name'] = for_user_id

    if only_global:
        conditions.append(" ak.for_user_id is null ")

    if api_key_id is not None:
        assert isinstance(api_key_id, int)
        conditions.append(" ak.api_key_id = %(key_id)s ")
        query_params['key_id'] = api_key_id

    query = '''
            with t1 as (
                select api_key_id, right(api_key, 5) as short_key, key_name, ak.start_timestamp,
                    first_name || ' ' ||  last_name as creator, is_enabled, api_version, access_type,
                    ip_restrictions, for_user_id
                from api_keys as ak
                join users on ak.created_by = users.user_id
                where ak.organization_id = %(org_id)s
                    and ak.start_timestamp <= %(timestamp)s
                    and ak.end_timestamp > %(timestamp)s
                    and users.start_timestamp <= %(timestamp)s
                    and users.end_timestamp > %(timestamp)s
                    {0}
            )
            , t2 as (
                select api_key_id, max(log_timestamp) as last_call
                from api_call_logs
                where api_key_id in (select api_key_id from t1)
                    and organization_id = %(org_id)s
                group by api_key_id
            )
            select t1.short_key, key_name, start_timestamp, creator, is_enabled, api_version, access_type,
                ip_restrictions, for_user_id, last_call
            from t1 left join t2 using(api_key_id)
            order by start_timestamp;
            '''.format(' and ' + ' and '.join(conditions) if len(conditions) > 0 else '')
    try:
        result = conn.fetch(query, query_params)
        data = []
        for key_, name_, created_on_, created_by_, is_enabled_, version_, access_, ip_, for_user_, last_use_ in result:
            data.append({
                var_names.key: key_,
                var_names.key_name: name_,
                var_names.created_on: created_on_,
                var_names.created_by: created_by_,
                var_names.is_enabled: is_enabled_,
                var_names.api_version: version_,
                var_names.access_level: access_,
                var_names.ip_address: ip_,
                var_names.for_user: for_user_,
                var_names.last_usage_timestamp: last_use_
            })
        return data
    except psycopg2.DatabaseError:
        raise


def get_enabled_api_key_accepting_info(conn, timestamp, api_key=None):
    '''
    Get API keys given certain parameters.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param api_key: API key to filter by
    :return: (dict of list) -> {api key: [key id, org id, perm, for user, ip restrictions]}
    '''
    assert isinstance(timestamp, datetime.datetime)
    query_params = {'timestamp': timestamp}

    cond = ''
    if api_key is not None:
        cond = ' and api_key = %(key)s '
        query_params['key'] = api_key

    query = '''
            select api_key, api_key_id, organization_id, permissions, for_user_id, ip_restrictions
            from api_keys
            where start_timestamp <= %(timestamp)s
                and end_timestamp > %(timestamp)s
                and is_enabled = true
                {0};
            '''.format(cond)
    try:
        result = conn.fetch(query, query_params)
        data = dict()
        for key_, id_, org_id_, perm_, user_, ip_ in result:
            data[key_] = [id_, org_id_, perm_, user_, ip_]
        return data
    except psycopg2.DatabaseError:
        raise


def get_owner_if_api_key_user_specific(conn, timestamp, api_key_id):
    '''
    Get the owner of an API key if it is user specific.
    :param conn: db connection
    :param timestamp: timestamp this request is being made on
    :param api_key_id: ID of the API key
    :return: user_id of the owner or None if it the API key is not user specific
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(api_key_id, int)
    query = '''
            select for_user_id from api_keys
            where start_timestamp <= %s
                and end_timestamp > %s
                and api_key_id = %s;
            '''
    query_params = (timestamp, timestamp, api_key_id,)
    try:
        result = conn.fetch(query, query_params)
        if len(result) == 1:
            return result[0][0]
        elif len(result) == 0:
            raise LookupError(errors.err_unknown_resource)
        else:
            raise SystemError(errors.err_internal_multiple_entries_found)
    except psycopg2.DatabaseError:
        raise


def log_api_call(conn, timestamp, organization_id, api_key_id, api_version, event_type=None, instance_id=None,
                 denied=None, log=None):
    '''
    Log an api call.
    :param conn: db connection
    :param timestamp: timestamp this request is being made on
    :param organization_id: ID of the organization the api key belongs to
    :param api_key_id: ID of the api key
    :param api_version: the version/type of API this log is for
    :param event_type: the type of event the call was made for
    :param instance_id: instance ID the call was made for
    :param denied: (boolean) True if the call was denied
    :param log: (optional) any log for the call
    :errors: AssertionError, DatabaseError
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(organization_id, int)
    assert isinstance(api_key_id, int)
    assert api_version in configs.allowed_api_versions
    if event_type is not None:
        assert isinstance(event_type, str)
    if instance_id is not None:
        assert isinstance(instance_id, int)
    if denied is not None:
        assert isinstance(denied, bool)

    query = '''
            insert into api_call_logs (
                log_date, log_timestamp, api_key_id, organization_id, event_type,
                instanceid, request_denied, request_log, api_version
            ) values (
                %s, %s, %s, %s, %s,
                %s, %s, %s, %s
            );
            '''
    query_params = (timestamp.date(), timestamp, api_key_id, organization_id, event_type,
                    instance_id, denied, log, api_version,)
    try:
        conn.execute(query, query_params)
    except psycopg2.DatabaseError:
        raise


def is_api_key_name_unique(conn, timestamp, organization_id, key_name):
    '''
    Checks if an api key name is unique inside the organization or not.
    :param conn: db connection.
    :param timestamp: timestamp when this request is being made
    :param organization_id: ID of the organization to look at
    :param key_name: name of the key to check for
    :return: (boolean) True if it is unique; False otherwise
    :errors: AssertionError, DatabaseError
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(organization_id, int)
    assert string_validator.is_standard_name(key_name)

    query = '''
            select key_name from api_keys
            where start_timestamp <= %s
                and end_timestamp > %s
                and organization_id = %s;
            '''
    query_params = (timestamp, timestamp, organization_id,)
    try:
        result = conn.fetch(query, query_params)
        current_key_names = []
        for item in result:
            current_key_names.append(item[0])

        if key_name in current_key_names:
            return False
        else:
            return True
    except psycopg2.DatabaseError:
        raise


def get_api_key_id_from_sub_key_and_name(conn, timestamp, organization_id, sub_key, key_name):
    '''
    Get the api key ID from the last 5 characters of the api key and the key name.
    :param conn: db connection
    :param timestamp: timestamp when the request is being made
    :param organization_id: ID of the organization
    :param sub_key: (str) last 5 characters of the api key
    :param key_name: (str) name of the key
    :return: (int) api key ID
    :errors: AssertionError, DatabaseError, LookupError, SystemError
    '''
    assert isinstance(timestamp, datetime.datetime)
    assert isinstance(organization_id, int)
    assert isinstance(sub_key, str)
    assert string_validator.is_standard_name(key_name)

    query = '''
            select api_key_id from api_keys
            where start_timestamp <= %(timestamp)s
                and end_timestamp > %(timestamp)s
                and organization_id = %(org_id)s
                and right(api_key, 5) = %(sub_key)s
                and key_name = %(k_name)s;
            '''
    query_params = {'timestamp': timestamp, 'org_id': organization_id,
                    'sub_key': sub_key, 'k_name': key_name}
    try:
        result = conn.fetch(query, query_params)
        if len(result) == 1:
            return result[0][0]
        elif len(result) == 0:
            raise LookupError(errors.err_unknown_resource)
        else:
            raise SystemError(errors.err_internal_multiple_entries_found)
    except psycopg2.DatabaseError:
        raise
