# import contextlib
# import functools
import contextlib
import io
import json
import os

# from dcicutils.misc_utils import local_attrs
from dcicutils.exceptions import AppEnvKeyMissing, AppServerKeyMissing
from dcicutils.misc_utils import remove_suffix


# LOCAL_SERVER = "http://localhost:8000"
# LOCAL_PSEUDOENV = 'fourfront-cgaplocal'
#
# PRODUCTION_SERVER = 'https://cgap.hms.harvard.edu'
# PRODUCTION_ENV = 'fourfront-cgap'
#
# keys_file_var='CGAP_KEYS_FILE'


_KEY_MANAGERS = {}


class KeyManager:

    APP_NAME = None
    APP_TOKEN = None  # Set this to a string to override APP_NAME.lower() as the app token

    KEYS_FILE_VAR = None

    _REGISTERED = False

    # Instance Methods

    def __init__(self, keys_file=None):
        if not self._REGISTERED:
            raise RuntimeError("Only registered KeyManagers can be instantiated.")
        self.keys_file = keys_file or os.environ.get(self.KEYS_FILE_VAR) or self.KEYS_FILE
        if not self.keys_file:
            raise ValueError(f"No KEYS_FILE attribute in {self}, and no {self.KEYS_FILE_VAR} environment variable.")

    KEYS_FILE = None
    # @property
    # def KEYS_FILE(self):  # noQA
    #     # By default this will be computed dynamically, but a KeyManager class can declare a static value by doing
    #     # something like this:
    #     #
    #     #     class MyKeyManager(KeyManager):
    #     #         KEYS_FILE = 'some file'
    #     #
    #     return self._default_keys_file()

    @classmethod
    @contextlib.contextmanager
    def default_keys_file_for_testing(cls, filename):
        """
        Sets the default keys file for in cls.KEYS_FILE to the indicated filename.
        Ordinarily, in a non-testing environment, one would set an environment variable to do this,
        but in the testing environment that has been set already and to a value that has nothing to
        do with testing. So this bypasses the normal environment variable setup and sets it directly
        in the class and only for the duration of an evaluation context.
        """

        old_filename = cls.KEYS_FILE
        try:
            cls.KEYS_FILE = filename or cls._default_keys_file()
            yield
        finally:
            cls.KEYS_FILE = old_filename

    @classmethod
    def register(cls, *, name):
        def _register_class(key_manager_class):
            assert issubclass(key_manager_class, KeyManager)
            if name in _KEY_MANAGERS:
                raise ValueError(f"A KeyManager named {name!r} has already been defined.")
            key_manager_class._init_class_variables()
            key_manager_class._REGISTERED = True
            _KEY_MANAGERS[name] = cls
            return key_manager_class
        return _register_class

    @classmethod
    def _init_class_variables(cls):
        class_name = cls.__name__
        print(f"cls={cls!r}, name={class_name!r}")
        suffix = "KeyManager"
        if not class_name.endswith(suffix):
            raise ValueError(f"The name, {class_name!r}, of a KeyManager class to be registered"
                             f" does not end in {suffix!r}.")
        app_name = remove_suffix(suffix=suffix, text=class_name, required=True)
        if cls.APP_NAME is None:
            cls.APP_NAME = app_name
        elif cls.APP_NAME.lower() != app_name.lower():
            raise ValueError(f"A KeyManager class with APP_NAME = {cls.APP_NAME!r} expects a name"
                             f" like {cls.APP_NAME}{suffix}.")
        else:
            app_name = cls.APP_NAME
        if cls.APP_TOKEN is None:
            cls.APP_TOKEN = app_token = app_name.upper()
        else:
            app_token = cls.APP_TOKEN
            if not app_token.isupper():
                raise ValueError(f"The APP_TOKEN {app_token!r} must be all-uppercase.")
            elif app_token[:1] != app_name[0].upper():
                raise ValueError(f"The APP_TOKEN {app_token!r} must have the same first letter as {app_name!r}")
        app_prefix = app_token + "_"
        if not cls.KEYS_FILE_VAR:
            cls.KEYS_FILE_VAR = f"{app_prefix}KEYS_FILE"
        elif not cls.KEYS_FILE_VAR.startswith(app_prefix):
            raise ValueError("The {class_name}.KEYS_FILE_VAR must begin with {app_prefix!r}.")
        if not cls.KEYS_FILE:
            cls.KEYS_FILE = cls._default_keys_file()
        # print(f"Defining {class_name} "
        #       f"with APP_NAME={app_name!r} APP_TOKEN={app_token!r} KEYS_FILE_VAR={cls.KEYS_FILE_VAR!r}.")

    @classmethod
    def create(cls, name, **kwargs):
        key_manager_class = _KEY_MANAGERS.get(name)
        if not key_manager_class:
            raise ValueError(f"There is no registered KeyManager class named {name!r}.")
        return key_manager_class(**kwargs)

    # @contextlib.contextmanager
    # def alternate_keys_file_from_environ(self):
    #     filename = os.environ.get(self.KEYS_FILE_VAR) or None  # Treats empty string as undefined
    #     with self.alternate_keys_file(filename=filename):
    #         yield

    # Class Methods

    @classmethod
    def _default_keys_file(cls):
        return os.path.expanduser(f"~/.{cls.APP_TOKEN.lower()}-keys.json")

    # Replaced by an instance variable. Rewrites weill be needed.
    #
    # @classmethod
    # def keys_file(cls):
    #     app_token = cls.APP_TOKEN or cls.APP_NAME.lower()
    #     return os.path.expanduser(cls.KEYS_FILE or f"~/.{app_token}-keys.json")

    # # Formerly a function / classmethod, now a normal instance method of an instance (of KeyManager)
    # @contextlib.contextmanager
    # def alternate_keys_file(self, filename):
    #     if filename is None:
    #         yield  # If no alternate filename given, change nothing
    #     else:
    #         with local_attrs(self, keys_file=filename):
    #             yield

    # @classmethod
    # def UsingKeysFile(cls, key_manager):
    #     def _UsingKeysFile(fn):  # TO DO: declare as decorator
    #         @functools.wraps(fn)
    #         def wrapped(*args, **kwargs):
    #             with key_manager.alternate_keys_file_from_environ():
    #                 return fn(*args, **kwargs)
    #         return wrapped
    #     return _UsingKeysFile

    @classmethod  # tested. good as class method
    def keypair_to_keydict(cls, auth_tuple, *, server):
        auth_dict = {
            'key': auth_tuple[0],
            'secret': auth_tuple[1],
            'server': server,
        }
        return auth_dict

    @classmethod  # tested. good as class method
    def keydict_to_keypair(cls, auth_dict):
        return (
            auth_dict['key'],
            auth_dict['secret']
        )

    # formerly a class method. no longer can be.
    def get_keydicts(self):  # tested
        if not os.path.exists(self.keys_file):
            return {}
        with io.open(self.keys_file) as fp:
            keys = json.load(fp)
            return keys

    @classmethod
    def _check_env(cls, env):
        if not env or not isinstance(env, str):
            raise ValueError(f"Not a valid environment: {env}")

    def get_keydict_for_env(self, env):
        """
        Gets the appropriate auth info (as a dict) for talking to a given beanstalk environment.

        Args:
            env: the name of a beanstalk environment

        Returns:
            Auth information as a dict with keys 'key', 'secret', and 'server'.
        """

        self._check_env(env)

        keydicts = self.get_keydicts()
        keydict = keydicts.get(env)
        if not keydict:
            raise AppEnvKeyMissing(env=env, key_manager=self)
        return keydict

    def get_keypair_for_env(self, env):
        """
        Gets the appropriate auth info (as a pair/tuple) for talking to a given beanstalk environment.

        Args:
            env: the name of an environment

        Returns:
            Auth information as a (key, secret) tuple.
        """

        return self.keydict_to_keypair(self.get_keydict_for_env(env=env))

    @classmethod
    def _check_server(cls, server):
        if not server or not isinstance(server, str):
            raise ValueError(f"Not a valid server: {server}")

    def get_keydict_for_server(self, server):  # tested
        """
        Gets the appropriate auth info (as a dict) for talking to a given beanstalk environment.

        Args:
            server: the name of a server

        Returns:
            Auth information.
            The auth is a keypair, though we might change this to include a JWT token in the the future.
        """

        self._check_server(server)

        keydicts = self.get_keydicts()
        server_to_find = server.rstrip('/')
        for keydict in keydicts.values():
            if keydict['server'].rstrip('/') == server_to_find:
                return keydict
        raise AppServerKeyMissing(server=server, key_manager=self)

    def get_keypair_for_server(self, server):  # tested
        """
        Gets the appropriate auth info (as a pair/tuple) for talking to a given beanstalk environment.

        Args:
            server: the name of a server

        Returns:
            Auth information as a (key, secret) tuple.
        """

        return self.keydict_to_keypair(self.get_keydict_for_server(server=server))


@KeyManager.register(name='cgap')
class CGAPKeyManager(KeyManager):
    pass


@KeyManager.register(name='fourfront')
class FourfrontKeyManager(KeyManager):
    pass
