import copy
#################################################
# 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 logging

from holado_core.common.exceptions.functional_exception import FunctionalException
from holado_rest.api.rest.rest_client import RestClient
import socket
import urllib.parse
import re
from holado_rest.api.rest.rest_manager import RestManager
from holado.common.handlers.undefined import undefined_argument, undefined_value
import os
from holado_core.common.tools.converters.converter import Converter
from holado_core.common.exceptions.technical_exception import TechnicalException
from holado_core.common.handlers.wait import WaitFuncResult

logger = logging.getLogger(__name__)


class KeycloakClient(RestClient):
    """
    Helper class to interact with the Keycloak REST API.
    It inherits from RestClient all methods of a REST client, thus any endpoint can be called.
    Some methods are added for commonly used endpoints, and illustrates how use this client for other endpoints.
    """

    @classmethod
    def new_client(cls, use_localhost=undefined_argument, **kwargs):
        if 'name' not in kwargs:
            kwargs['name'] = None
        if 'url' not in kwargs:
            if use_localhost is undefined_argument:
                env_use = os.getenv("HOLADO_USE_LOCALHOST", False)
                use_localhost = Converter.is_boolean(env_use) and Converter.to_boolean(env_use)
            
            scheme = kwargs.get('scheme', 'http')
            host = kwargs.get('host', 'localhost' if use_localhost else undefined_value)
            if host is undefined_value:
                raise FunctionalException("Host or url must be defined")
            port = kwargs.get('port', None)

            if port is None:
                url = f"{scheme}://{host}"
            else:
                url = f"{scheme}://{host}:{port}"
            kwargs['url'] = url

        # Force content-type to application/x-www-form-urlencoded

        manager = RestManager(default_client_class=cls)
        res = manager.new_client(**kwargs)
        
        return res
    
    
    def __init__(self, name, url, headers=None):
        super().__init__(name, url, headers)

    def get_token(self, **kwargs):
        if 'username' in kwargs and 'password' in kwargs:
            return self.get_token_by_grant_type_password(**kwargs)
        elif 'refresh_token' in kwargs:
            return self.get_token_by_grant_type_refresh_token(**kwargs)
        elif 'client_id' in kwargs and 'client_secret' in kwargs:
            return self.get_token_by_grant_type_client_credentials(**kwargs)
        else:
            raise FunctionalException(
                f"Unable to get token with given parameters.\nAllowed methods and associated parameters: password -> ('username', 'password'), refresh -> ('refresh_token'), client credentials -> ('client_id', 'client_secret')\nReceived parameters: {kwargs}")

    def get_token_by_grant_type_password(self, realm_name, username, password, client_id=None, client_secret=None):
        data = {
            'grant_type': 'password',
            'username': username,
            'password': password,
        }
        if client_id:
            data['client_id'] = client_id
        if client_secret:
            data['client_secret'] = client_secret

        return self.post(f"realms/{realm_name}/protocol/openid-connect/token", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})

    def get_token_by_grant_type_refresh_token(self, realm_name, refresh_token, client_id=None, client_secret=None):
        data = {
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
        }
        if client_id:
            data['client_id'] = client_id
        if client_secret:
            data['client_secret'] = client_secret

        return self.post(f"realms/{realm_name}/protocol/openid-connect/token", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})

    def get_token_by_grant_type_client_credentials(self, realm_name, client_id=None, client_secret=None):
        data = {
            'grant_type': 'client_credentials',
        }
        if client_id:
            data['client_id'] = client_id
        if client_secret:
            data['client_secret'] = client_secret

        return self.post(f"realms/{realm_name}/protocol/openid-connect/token", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})

    def logout(self, **kwargs):
        """ Logout from Keycloak
        Note: only method with refresh_token seems to work, thus only this method is currently managed.
        """
        if 'refresh_token' in kwargs:
            return self.logout_by_refresh_token(**kwargs)
        # NOTE: As described below, method by token should work but doesn't, that's why it is commented
        # elif 'token' in params:
        #     client.logout_by_token(**params)
        else:
            raise FunctionalException(f"Unable to logout with given parameters.\nAllowed methods and associated parameters: refresh token -> ('refresh_token')\nReceived parameters: {kwargs}")

    # NOTE: This method doesn't work whereas it should according to documentation. Use instead logout_by_refresh_token.
    # def logout_by_token(self, realm_name, token, client_id=None, client_secret=None):
    #     data = {
    #         'id_token_hint': token,
    #     }
    #     if client:
    #         data['client_id'] = client[0]
    #         data['client_secret'] = client[1]
    #
    #     return self.post(f"realms/{realm_name}/protocol/openid-connect/logout", json=data)

    def logout_by_refresh_token(self, realm_name, refresh_token, client_id=None, client_secret=None):
        data = {
            'refresh_token': refresh_token,
        }
        if client_id:
            data['client_id'] = client_id
        if client_secret:
            data['client_secret'] = client_secret

        return self.post(f"realms/{realm_name}/protocol/openid-connect/logout", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})

    def get_user_info(self, realm_name, username, client_id=None, client_secret=None, field_name=None, field_default_value=None, do_raise_exception=True):
        data = {}
        if client_id:
            data['client_id'] = client_id
        if client_secret:
            data['client_secret'] = client_secret

        path_parameters = {
            'exact': True,
            'username': username,
        }

        if data:
            result = self.get(f"admin/realms/{realm_name}/users", data=data, path_parameters=path_parameters, headers={"Content-Type": "application/x-www-form-urlencoded"})
        else:
            result = self.get(f"admin/realms/{realm_name}/users", path_parameters=path_parameters)

        if len(result) == 0:
            if do_raise_exception:
                raise FunctionalException(f"Unable to find information on user '{username}'.")
            else:
                return None
        elif len(result) > 1:
            raise TechnicalException(f"Found {len(result)} users for user '{username}': {result}")
        res = result[0]

        if field_name is not None:
            res = res.get(field_name, field_default_value)
        return res

    def get_user_required_actions(self, realm_name, username, client_id=None, client_secret=None):
        return self.get_user_info(realm_name, username, client_id=client_id, client_secret=client_secret, field_name="requiredActions", field_default_value=[])

    def get_user_id(self, realm_name, username, client_id=None, client_secret=None):
        user = self.get_user_info(realm_name, username, client_id=client_id, client_secret=client_secret)
        return user['id']

    def verify_user_exists(self, realm_name, username, do_raise_exception=True):
        user_info = self.get_user_info(realm_name, username, do_raise_exception=do_raise_exception)
        return user_info is not None

    def create_user(self, realm_name, username, **user_data):
        data = copy.deepcopy(user_data)
        data['username'] = username

        return self.post(f"admin/realms/{realm_name}/users", json=data)

    def set_user_password(self, realm_name, username, password, client_id=None, client_secret=None):
        user_id = self.get_user_id(realm_name, username, client_id=client_id, client_secret=client_secret)

        data = {
            'type': 'password',
            'temporary': False,
            'value': password,
        }
        if client_id:
            data['client_id'] = client_id
        if client_secret:
            data['client_secret'] = client_secret

        # return self.put(f"admin/realms/{realm_name}/users/{user_id}/reset-password", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})
        return self.put(f"admin/realms/{realm_name}/users/{user_id}/reset-password", json=data)

    def get_client_info(self, realm_name, client_id, do_raise_exception=True):
        path_parameters = {
            'clientId': client_id,
        }

        result = self.get(f"admin/realms/{realm_name}/clients", path_parameters=path_parameters)

        if len(result) == 0:
            if do_raise_exception:
                raise FunctionalException(f"Unable to find information on client '{client_id}'.")
            else:
                return None
        elif len(result) > 1:
            raise TechnicalException(f"Found {len(result)} clients for client '{client_id}': {result}")
        return result[0]

    def verify_client_exists(self, realm_name, client_id, do_raise_exception=True):
        client_info = self.get_client_info(realm_name, client_id, do_raise_exception=do_raise_exception)
        return client_info is not None

    def create_client(self, realm_name, client_id, **client_data):
        data = copy.deepcopy(client_data)
        data['clientId'] = client_id

        # return self.post(f"admin/realms/{realm_name}/clients", data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})
        return self.post(f"admin/realms/{realm_name}/clients", json=data)

    def verify_user_has_required_action(self, realm_name, username, action, check_has=True, do_raise_exception=True):
        required_actions = self.get_user_required_actions(realm_name, username)
        if check_has:
            res = action in required_actions
        else:
            res = action not in required_actions

        if not res and do_raise_exception:
            raise FunctionalException(f"User '{username}' has{' not' if check_has else ''} required action '{action}' (required actions: {required_actions}).")
        return res

    def process_automatically_user_required_actions(self, realm_name, username):
        """
        Process automatically some user actions.
        :param realm_name: realm name
        :param username: username
        """
        required_actions = self.get_user_required_actions(realm_name, username)
        # Do a loop as workflows can create new actions
        while required_actions:
            # Process actions
            for action in required_actions:
                if action == "VERIFY_EMAIL":
                    self.process_action_user_verify_email(realm_name, username)
                else:
                    raise FunctionalException(f"Unmanaged user action '{action}'")

            required_actions = self.get_user_required_actions(realm_name, username)

    def process_action_user_verify_email(self, realm_name, username):
        user_id = self.get_user_id(realm_name, username)

        data = {"emailVerified": True}
        self.put(f"admin/realms/{realm_name}/users/{user_id}", json=data)

        # As user is forced with emailVerified=true, action was not really performed
        if self.verify_user_has_required_action(realm_name, username, action="VERIFY_EMAIL", do_raise_exception=False):
            self.remove_user_required_action(realm_name, username, action="VERIFY_EMAIL", user_id=user_id)
            self.verify_user_has_required_action(realm_name, username, action="VERIFY_EMAIL", check_has=False, do_raise_exception=True)

    def remove_user_required_action(self, realm_name, username, action, user_id=None):
        if user_id is None:
            user_id = self.get_user_id(realm_name, username)

        required_actions = self.get_user_required_actions(realm_name, username)
        if action not in required_actions:
            raise FunctionalException(f"Action '{action}' is not in required actions: {required_actions}.")

        required_actions.remove(action)
        data = {"requiredActions": required_actions}
        self.put(f"admin/realms/{realm_name}/users/{user_id}", json=data)

    def ensure_user_is_fully_set_up(self, realm_name, username):
        user_info = self.get_user_info(realm_name, username)

        # Ensure email is verified
        if not user_info["emailVerified"]:
            self.process_action_user_verify_email(realm_name, username)

