#################################################
# HolAdo (Holistic Automation do)
#
# (C) Copyright 2021-2025 by Eric Klumpp
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

# The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.
#################################################

import abc
import logging
from datetime import timedelta, datetime
from typing import NamedTuple

from holado.common.handlers.undefined import default
from holado_core.common.exceptions.technical_exception import TechnicalException
from holado_core.common.tools.tools import Tools
from holado_python.common.tools.datetime import DateTime


logger = logging.getLogger(__name__)

class TokenManager(object):
    """
    Manage tokens, by storing tokens and requesting/refreshing when needed.
    This class is abstract, method request_token needs to be overridden.
    Note: to allow the possibility to modify method signatures, all methods parameters are named.
    """
    __metaclass__ = abc.ABCMeta

    def __init__(self):
        self.__token_info_by_uid = {}

    def has_token(self, *, uid):
        return uid in self.__token_info_by_uid

    def get_token_info(self, *, uid, do_raise_exception=True):
        if self.has_token(uid=uid):
            res = self.__token_info_by_uid[uid]
        else:
            if do_raise_exception:
                raise TechnicalException(f"UID '{uid}' has no token")
            else:
                res = None
        if Tools.do_log(logger, logging.TRACE):
            logger.trace(f"Token info of UID '{uid}': {Tools.represent_object(res)}")
        return res

    def _set_token_info(self, *, uid, token, request_kwargs=None):
        self.__token_info_by_uid[uid] = self._new_token_info(token=token, request_kwargs=request_kwargs)

    def delete_token(self, *, uid):
        if self.has_token(uid=uid):
            del self.__token_info_by_uid[uid]
        else:
            raise TechnicalException(f"UID '{uid}' has no token")

    def get_token(self, *, uid, request_kwargs=None):
        """Get token for given UID, and if needed request token with given request_kwargs"""
        if self.has_token(uid=uid):
            return self.get_token_info(uid=uid).token
        else:
            request_kwargs = request_kwargs if request_kwargs is not None else {}
            res = self.request_token(**request_kwargs)
            self._set_token_info(uid=uid, token=res, request_kwargs=request_kwargs)
            return res

    def _new_token_info(self, *, token, request_kwargs=None):
        """Create a new token info object containing all needed information related to token.
        Note: Parameter 'request_kwargs' is not used by default, it exists for use cases needing to store some of these information.
              In this case, this method must be overridden with expected behavior.
        """
        res = NamedTuple("TokenInfo", token=dict, creation_date=datetime)
        res.token = token
        res.creation_date = DateTime.now()
        return res

    def request_token(self, **request_kwargs):
        raise NotImplementedError()


class ExpiringTokenManager(TokenManager):
    """
    Manage tokens with expiration.
    This class is abstract, methods request_token and request_refresh need to be overridden.
    """
    __metaclass__ = abc.ABCMeta

    GLOBAL_DEFAULT_NEEDED_TIME_S = 30

    def __init__(self, with_refresh=True):
        super().__init__()
        self.__default_needed_time_s = self.GLOBAL_DEFAULT_NEEDED_TIME_S
        self.__with_refresh = with_refresh

    @property
    def with_refresh(self):
        return self.__with_refresh

    @property
    def default_needed_time_s(self):
        return self.__default_needed_time_s

    @default_needed_time_s.setter
    def default_needed_time_s(self, needed_time_s):
        self.__default_needed_time_s= needed_time_s

    def get_token(self, *, uid, request_kwargs=None, needed_time_s=default):
        if self.has_token(uid=uid):
            if request_kwargs is not None:
                # Note: if request_kwargs is not defined, it is not possible to make any request
                self.ensure_token_is_valid(uid=uid, request_kwargs=request_kwargs, needed_time_s=needed_time_s)
            return self.get_token_info(uid=uid).token
        else:
            return super().get_token(uid=uid, request_kwargs=request_kwargs)

    def _new_token_info(self, *, token, request_kwargs=None):
        """Create a new token info object containing all needed information related to token.
        Note: This implementation prepare a NamedTuple with field 'expiration_date', and 'refresh_expiration_date' if with_refresh, but is not able to fill them,
              thus this method has to be overridden to complete these fields.
        """
        if self.__with_refresh:
            res = NamedTuple("TokenInfo", token=dict, creation_date=datetime, expiration_date=datetime, refresh_expiration_date=datetime)
        else:
            res = NamedTuple("TokenInfo", token=dict, creation_date=datetime, expiration_date=datetime)
        res.token = token
        res.creation_date = DateTime.now()
        return res

    def ensure_token_is_valid(self, *, uid, request_kwargs=None, needed_time_s=default):
        request_kwargs = request_kwargs if request_kwargs is not None else {}
        needed_time_s = needed_time_s if needed_time_s is not default else self.default_needed_time_s

        # Note: this method is supposed to be called when a token exists already, thus do_raise_exception is True
        ti = self.get_token_info(uid=uid, do_raise_exception=True)

        now = DateTime.now()
        needed_end = now + timedelta(seconds=needed_time_s)
        if needed_end > ti.expiration_date:
            if self.with_refresh and ti.refresh_expiration_date > now:
                token = self.request_refresh(uid=uid, **request_kwargs)
            else:
                token = self.request_token(**request_kwargs)
            self._set_token_info(uid=uid, token=token)

    def request_refresh(self, *, uid, **request_kwargs):
        """ Process refresh request.
        Note: given input kwargs are those used for the request.
        """
        raise NotImplementedError()


