import json
from multiprocessing.pool import ThreadPool
import requests
from base64 import b64encode

from fylr_lib_plugin_python3 import util

PLUGIN_NAME = 'easydb-connector-plugin'

# helpers


def load_json(j_str):
    try:
        return json.loads(j_str), None
    except Exception as e:
        return None, str(e)


def parseurl_params(parameters):

    url_params = {}

    # easydb5
    query_string_parameters = util.get_json_value(parameters, 'query_string_parameters')
    if query_string_parameters:
        return query_string_parameters

    query_string = util.get_json_value(parameters, 'query_string')
    if query_string:
        pairs = query_string.split('&')

        for p in pairs:
            param = p.split('=')
            if len(param) != 2:
                continue
            if len(param[0]) < 1 or len(param[1]) < 1:
                continue
            url_params[param[0]] = param[1]

        return url_params

    query = util.get_json_value(parameters, 'query')
    if isinstance(query, dict):
        for k in query:
            v = query[k]
            if not isinstance(v, list):
                continue
            if len(v) < 1:
                continue
            url_params[k] = v
        return url_params

    return url_params


# system rights checks
SYSTEM_RIGHT_ALLOW_USE = f'plugin.{PLUGIN_NAME}.allow_use'
SYSTEM_RIGHT_ALLOW_USE_AS_SERVER = f'plugin.{PLUGIN_NAME}.allow_use_as_server'


def check_system_right(session, right):

    system_rights = util.get_json_value(session, 'system_rights')
    if not isinstance(system_rights, dict):
        return False

    if 'system.root' in system_rights:
        return True
    return right in system_rights


def check_connector_use_right(session):
    if not check_system_right(session, SYSTEM_RIGHT_ALLOW_USE):
        raise NoRightToUse()


def check_connector_use_as_server_right(session):
    if not check_system_right(session, SYSTEM_RIGHT_ALLOW_USE_AS_SERVER):
        raise NoRightToUseAsServer()


# errors and exceptions


def format_error(msg):
    return {
        'msg': msg,
    }


def response_status_not_ok_error(status_code, text):
    parsed_resp, error = load_json(text)
    if error:
        return {
            'status': status_code,
            'msg': text,
        }

    return {
        'status': status_code,
        'msg': parsed_resp,
    }


def format_error_response(code, parameters={}):
    err = {
        'realm': 'user',
        'code': code,
    }
    if parameters != {}:
        err['parameters'] = parameters
    return err


class NotAuthenticated(Exception):
    def __init__(self):
        Exception()

    def __str__(self):
        return 'error.user.not_authenticated'


class NotEnabled(Exception):
    def __init__(self):
        Exception()


class NoSystemRight(Exception):
    def __init__(self):
        Exception()

    def __str__(self):
        return 'error.user.no_system_right'


class NoRightToUse(NoSystemRight):
    def __init__(self):
        NoSystemRight()

    def get_right(self):
        return 'allow_use'


class NoRightToUseAsServer(NoSystemRight):
    def __init__(self):
        NoSystemRight()

    def get_right(self):
        return 'allow_use_as_server'


class ConnectorConfig(object):
    def __init__(self, plugin_base_config, cc):
        self.name = util.get_json_value(cc, 'name')
        self.url = util.get_json_value(cc, 'url')
        if self.url is None:
            raise Exception('no connector url')
        self.login = util.get_json_value(cc, 'login')
        self.pw = util.get_json_value(cc, 'password')
        self.client_id = util.get_json_value(cc, 'client_id')
        self.client_secret = util.get_json_value(cc, 'client_secret')
        self.version = None

        _https_insecure = util.get_json_value(cc, 'https_insecure')
        self.https_insecure = _https_insecure == True

        self.timeout_sec = util.get_json_value(cc, 'timeout_sec')
        if isinstance(self.timeout_sec, int):
            if self.timeout_sec < 0:
                self.timeout_sec = 0
        else:
            # use default value from plugin configuration
            got_default = False

            if isinstance(plugin_base_config, list):
                for conf in plugin_base_config:

                    name = util.get_json_value(conf, 'name')
                    if name != 'connector':
                        continue

                    fields = util.get_json_value(conf, 'parameters.easydbs.fields')
                    if not isinstance(fields, list):
                        continue

                    for field in fields:
                        name = util.get_json_value(field, 'name')
                        if name != 'timeout_sec':
                            continue

                        default = util.get_json_value(field, 'default')
                        if not isinstance(default, int):
                            continue

                        self.timeout_sec = default
                        got_default = True
                        break

                    if got_default:
                        break

            if not got_default:
                self.timeout_sec = 10


class Connector(object):
    def __init__(
        self,
        parameters,
        session,
        base_config,
        plugin_base_config,
        is_fylr,
        logger=None,
    ):
        self.parameters = parameters
        self.session = session
        self.base_config = base_config
        self.plugin_base_config = plugin_base_config

        self.is_fylr = is_fylr

        # only usable for easydb5
        self.logger = logger

    @classmethod
    def easydb_get(
        cls,
        url,
        endpoint,
        url_params={},
        https_insecure=False,
        timeout=0,
    ):
        if https_insecure:
            requests.packages.urllib3.disable_warnings(
                requests.packages.urllib3.exceptions.InsecureRequestWarning
            )
        try:
            r = requests.get(
                '%s/api/v1/%s' % (url, endpoint),
                params=url_params,
                verify=(not https_insecure),
                timeout=(timeout if timeout > 0 else None),
            )

            if r.status_code != 200:
                return None, response_status_not_ok_error(r.status_code, r.text)

            resp, error = load_json(r.text)
            if error:
                return None, f'could not parse response: {r.text}: ({error})'

            return resp, None
        except Exception as e:
            return None, str(e)

    @classmethod
    def easydb_post(
        cls,
        url,
        endpoint,
        url_params={},
        https_insecure=False,
        timeout=0,
    ):
        if https_insecure:
            requests.packages.urllib3.disable_warnings(
                requests.packages.urllib3.exceptions.InsecureRequestWarning
            )
        try:
            r = requests.post(
                '%s/api/v1/%s' % (url, endpoint),
                params=url_params,
                verify=(not https_insecure),
                timeout=(timeout if timeout > 0 else None),
            )

            if r.status_code != 200:
                return None, response_status_not_ok_error(r.status_code, r.text)

            resp, error = load_json(r.text)
            if error:
                return None, f'could not parse response: {r.text}: ({error})'

            return resp, None
        except Exception as e:
            return None, str(e)

    @classmethod
    def login_connector(cls, cconf):
        try:
            # get the settings of the remote instance to check if it is a easydb5 or fylr instance
            response, error = cls.easydb_get(
                url=cconf.url,
                endpoint='settings',
                https_insecure=cconf.https_insecure,
                timeout=cconf.timeout_sec,
            )

            if error:
                return cconf, None, error

            version = util.get_json_value(response, 'version')

            if version is None:
                return cconf, None, 'could not find server version'

            cconf.version = version

            if version.startswith('v5.'):
                return cls.authenticate_easydb5(cconf)

            if version.startswith('v6.'):
                return cls.authenticate_fylr(cconf)

            raise Exception('unknown server version ' + version)

        except NoRightToUseAsServer as e:
            return cconf, None, str(e)

    @classmethod
    def fylr_oauth2_password_token(cls, cconf):
        basic_auth = str(cconf.client_id) + ':' + str(cconf.client_secret)
        basic_auth_enc = b64encode(basic_auth.encode('ascii')).decode('ascii')

        try:
            r = requests.post(
                '%s/api/oauth2/token' % (cconf.url),
                data={
                    'grant_type': 'password',
                    'username': cconf.login,
                    'password': cconf.pw,
                },
                headers={
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Authorization': 'Basic %s' % basic_auth_enc,
                },
                timeout=30,
            )

            if r.status_code != 200:
                return None, response_status_not_ok_error(r.status_code, r.text)

            resp, error = load_json(r.text)
            if error:
                return None, f'could not parse response: {r.text}: ({error})'

            return util.get_json_value(resp, 'access_token'), None
        except Exception as e:
            return None, str(e)

    @classmethod
    def authenticate_fylr(cls, cconf):

        # get a OAuth2 access token
        access_token, error = cls.fylr_oauth2_password_token(cconf)
        if error:
            return cconf, None, error

        # log into session with the access token
        authenticated_session, error = cls.easydb_get(
            url=cconf.url,
            endpoint='user/session',
            url_params={
                'access_token': access_token,
            },
            https_insecure=cconf.https_insecure,
            timeout=cconf.timeout_sec,
        )

        if error:
            return cconf, None, error

        # confirm that we are authenticated
        auth = util.get_json_value(authenticated_session, 'authenticated.method')
        if auth != 'easydb':
            return (
                cconf,
                None,
                f'could not confirm that the session is authenticaed. expected "authenticated.method"="easydb", got "{auth}"',
            )

        check_connector_use_as_server_right(authenticated_session)

        return cconf, access_token, None

    @classmethod
    def authenticate_easydb5(cls, cconf):
        # get a new session token
        response, error = cls.easydb_get(
            url=cconf.url,
            endpoint='session',
            https_insecure=cconf.https_insecure,
            timeout=cconf.timeout_sec,
        )
        if error:
            return cconf, None, error

        new_token = util.get_json_value(response, 'token')
        if new_token is None:
            # no token in response, check if it is a easydb error
            if 'code' in response:
                error = response
            else:
                error = 'generic error: %s' % str(response)
            return cconf, None, error

        # authenticate the token
        query_params = {
            'method': 'easydb',
            'login': cconf.login,
            'password': cconf.pw,
            'token': new_token,
        }

        authenticated_session, error = cls.easydb_post(
            url=cconf.url,
            endpoint='session/authenticate',
            url_params=query_params,
            https_insecure=cconf.https_insecure,
            timeout=cconf.timeout_sec,
        )
        if error:
            return cconf, None, error

        auth_method = util.get_json_value(authenticated_session, 'authenticated.method')
        auth_token = util.get_json_value(authenticated_session, 'token')

        if not isinstance(auth_method, str) or not isinstance(auth_token, str):
            if 'code' in authenticated_session and 'realm' in authenticated_session:
                return (
                    cconf,
                    None,
                    {
                        'msg': authenticated_session,
                    },
                )
            else:
                return cconf, None, 'no token found in server response'

        check_connector_use_as_server_right(authenticated_session)

        return cconf, auth_token, None

    def get_connector_list(self):
        connectors = []

        self.parameters = parseurl_params(self.parameters)

        # check the rights of the user to use the connectors
        if self.session is None:
            raise NotAuthenticated()

        if self.is_fylr:
            enable_all = util.get_json_value(
                self.base_config,
                f'plugin.{PLUGIN_NAME}.config.connector.enable_all',
            )
        else:
            enable_all = util.get_json_value(
                self.base_config, 'system.connector.enable_all'
            )

        if enable_all is None or not isinstance(enable_all, bool):
            raise NotEnabled()
        if not enable_all:
            raise NotEnabled()

        get_tokens = False
        try:
            get_tokens = util.get_json_value(self.parameters, 'gettokens')[0] == '1'
        except:
            pass

        if self.is_fylr:
            connectors_config = util.get_json_value(
                self.base_config,
                f'plugin.{PLUGIN_NAME}.config.connector.easydbs',
            )
        else:
            connectors_config = util.get_json_value(
                self.base_config,
                'system.connector.easydbs',
            )

        if isinstance(connectors_config, list):
            configs = []
            for cc in connectors_config:

                enable = util.get_json_value(cc, 'enable')
                if enable is None or not isinstance(enable, bool):
                    continue
                if not enable:
                    continue

                # connect and authenticate to the connector to get a session token
                if get_tokens:
                    cconf = ConnectorConfig(self.plugin_base_config, cc)

                    configs.append(cconf)
                else:
                    connectors.append(
                        {
                            'name': util.get_json_value(cc, 'name'),
                            'url': util.get_json_value(cc, 'url'),
                        }
                    )

            if get_tokens:
                tp = ThreadPool(10)
                results = tp.imap_unordered(self.login_connector, configs)
                for cconf, token, error in results:
                    c = {
                        'name': cconf.name,
                        'url': cconf.url,
                        'version': cconf.version,
                    }

                    if token:
                        c['token'] = token
                    elif error:
                        c['error'] = error
                        if not isinstance(error, dict):
                            c['error'] = format_error(str(error))
                    connectors.append(c)

                tp.terminate()

        return {
            'easydbs': connectors,
        }
