from requests.exceptions import RequestsDependencyWarning
import warnings
import json
import requests
import os
import logging
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


warnings.filterwarnings("ignore", category=RequestsDependencyWarning)

# Configuración básica del logger

log_path = 'logs/sdk_logs.log'

log_dir = os.path.dirname(log_path)

if not os.path.exists(log_dir):
    os.makedirs(log_dir)

logging.basicConfig(
    level=logging.NOTSET,  # Nivel mínimo de severidad a registrar
    # Formato del mensaje de log
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(log_path),  # Registra en un archivo
        logging.StreamHandler()  # También muestra en la consola
    ]
)


class ServiceDiscoverer:
    class ServiceDiscovererException(Exception):
        pass

    def __init__(
            self,
            config_file
    ):
        # Cargar configuración desde archivo si es necesario
        config_file = os.path.abspath(config_file)
        config = self.__load_config_file(config_file)
        debug_mode = os.getenv('DEBUG_MODE', config.get(
            'debug_mode', 'False')).strip().lower()
        if debug_mode == "false":
            debug_mode = False

        # Inicializar logger
        self.logger = logging.getLogger(self.__class__.__name__)
        if debug_mode:
            self.logger.setLevel(logging.DEBUG)
        else:
            self.logger.setLevel(logging.WARNING)

        urllib_logger = logging.getLogger("urllib3")
        if not debug_mode:
            urllib_logger.setLevel(logging.WARNING)
        else:
            urllib_logger.setLevel(logging.DEBUG)

        self.config_path = os.path.dirname(config_file)+"/"
        capif_host = os.getenv(
            'CAPIF_HOST', config.get('capif_host', '')).strip()
        capif_https_port = str(
            os.getenv('CAPIF_HTTPS_PORT', config.get('capif_https_port', '')).strip())
        invoker_general_folder = os.path.abspath(
            os.getenv('invoker_folder', config.get('invoker_folder', '')).strip())

        capif_invoker_username = os.getenv(
            'CAPIF_USERNAME', config.get('capif_username', '')).strip()

        config = config["discover_filter"]
        self.discover_filter = {
            "api-name": os.getenv('API-NAME', config.get('api-name', '')).strip(),
            "api-version": os.getenv('API-VERSION', config.get('api-version', '')).strip(),
            "comm-type": os.getenv('COMM-TYPE', config.get('comm-type', '')).strip(),
            "protocol": os.getenv('PROTOCOL', config.get('protocol', '')).strip(),
            "aef-id": os.getenv('AEF-ID', config.get('aef-id', '')).strip(),
            "data-format": os.getenv('DATA-FORMAT', config.get('data-format', '')).strip(),
            "api-cat": os.getenv('API-CAT', config.get('api-cat', '')).strip(),
            "preferred-aef-loc": os.getenv('PREFERRED-AEF-LOC', config.get('preferred-aef-loc', '')).strip(),
            "req-api-prov-name": os.getenv('REQ-API-PROV-NAME', config.get('req-api-prov-name', '')).strip(),
            "supported-features": os.getenv('SUPPORTED-FEATURES', config.get('supported-features', '')).strip(),
            "api-supported-features": os.getenv('API-SUPPORTED-FEATURES', config.get('api-supported-features', '')).strip(),
            "ue-ip-addr": os.getenv('UE-IP-ADDR', config.get('ue-ip-addr', '')).strip(),
            "service-kpis": os.getenv('SERVICE-KPIS', config.get('service-kpis', '')).strip()
        }

        self.capif_invoker_username = capif_invoker_username
        self.capif_host = capif_host
        self.capif_https_port = capif_https_port
        self.invoker_folder = os.path.join(
            invoker_general_folder, capif_invoker_username
        )
        os.makedirs(self.invoker_folder, exist_ok=True)
        self.capif_api_details = self.__load_provider_api_details()

        self.signed_key_crt_path = os.path.join(
            self.invoker_folder, self.capif_api_details["user_name"] + ".crt"
        )
        self.private_key_path = os.path.join(
            self.invoker_folder, "private.key")
        self.ca_root_path = os.path.join(self.invoker_folder, "ca.crt")

        self.logger.info("ServiceDiscoverer initialized correctly")

    def get_api_provider_id(self):
        return self.capif_api_details["api_provider_id"]

    def __load_config_file(self, config_file: str):
        """Carga el archivo de configuración."""
        try:
            with open(config_file, 'r') as file:
                return json.load(file)
        except FileNotFoundError:
            self.logger.warning(
                f"Configuration file {config_file} not found. Using defaults or environment variables.")
            return {}

    def __load_provider_api_details(self):
        try:
            path = os.path.join(
                self.invoker_folder, "capif_api_security_context_details-"+self.capif_invoker_username+".json")
            with open(
                    path,
                    "r",
            ) as openfile:
                details = json.load(openfile)
            self.logger.info("Api provider details correctly loaded")
            return details
        except Exception as e:
            self.logger.error(
                "Error while loading Api invoker details: %s", str(e))
            raise

    def _add_trailing_slash_to_url_if_missing(self, url):
        if not url.endswith("/"):
            url += "/"
        return url

    def get_security_context(self):
        self.logger.info("Getting security context for all API's filtered")

        self.logger.info("Trying to update security context")
        self.__update_security_service()
        self.__cache_security_context()

    def get_access_token(self):
        """
        :param api_name: El nombre del API devuelto por descubrir servicios
        :param api_id: El id del API devuelto por descubrir servicios
        :param aef_id: El aef_id relevante devuelto por descubrir servicios
        :return: El token de acceso (jwt)
        """
        token_dic = self.__get_security_token()
        self.logger.info("Access token successfully obtained")
        return token_dic["access_token"]

    def __cache_security_context(self):
        try:
            path = os.path.join(
                self.invoker_folder, "capif_api_security_context_details-"+self.capif_invoker_username+".json")
            with open(
                    path, "w"
            ) as outfile:
                json.dump(self.capif_api_details, outfile)
            self.logger.info("Security context saved correctly")
        except Exception as e:
            self.logger.error(
                "Error when saving the security context: %s", str(e))
            raise

    def __update_security_service(self):
        """
        Actualiza el servicio de seguridad.

        :param api_id: El id del API devuelto por descubrir servicios.
        :param aef_id: El aef_id devuelto por descubrir servicios.
        :return: None.
        """
        url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/trustedInvokers/{self.capif_api_details['api_invoker_id']}/update"
        payload = {
            "securityInfo": [],
            "notificationDestination": "https://mynotificationdest.com",
            "requestTestNotification": True,
            "websockNotifConfig": {
                "websocketUri": "string",
                "requestWebsocketUri": True
            },
            "supportedFeatures": "fff"
        }

        number_of_apis = len(
            self.capif_api_details["registered_security_contexes"])

        for i in range(0, number_of_apis):
            # Obteniendo los valores de api_id y aef_id para cada API
            api_id = self.capif_api_details["registered_security_contexes"][i]["api_id"]
            aef_id = self.capif_api_details["registered_security_contexes"][i]["aef_id"]

            security_info = {
                "prefSecurityMethods": ["Oauth"],
                "authenticationInfo": "string",
                "authorizationInfo": "string",
                "aefId": aef_id,
                "apiId": api_id
            }

            payload["securityInfo"].append(security_info)

        try:
            response = requests.post(url,
                                     json=payload,
                                     cert=(self.signed_key_crt_path,
                                           self.private_key_path),
                                     verify=self.ca_root_path)
            response.raise_for_status()
            self.logger.info("Security context correctly updated")

        except requests.exceptions.HTTPError as http_err:
            if response.status_code == 404:
                self.logger.warning(
                    "Received 404 error, redirecting to register security service")
                self.__register_security_service()
            else:
                self.logger.error("HTTP error occurred: %s", str(http_err))
                raise

        except requests.RequestException as e:
            self.logger.error(
                "Error trying to update Security context: %s", str(e))
            raise

    def __register_security_service(self):
        """
        :param api_id: El id del API devuelto por descubrir servicios
        :param aef_id: El aef_id devuelto por descubrir servicios
        :return: None
        """

        url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/trustedInvokers/{self.capif_api_details['api_invoker_id']}"
        payload = {
            "securityInfo": [],
            "notificationDestination": "https://mynotificationdest.com",
            "requestTestNotification": True,
            "websockNotifConfig": {
                "websocketUri": "string",
                "requestWebsocketUri": True
            },
            "supportedFeatures": "fff"
        }

        number_of_apis = len(
            self.capif_api_details["registered_security_contexes"])

        for i in range(0, number_of_apis):
            # Obteniendo los valores de api_id y aef_id para cada API
            api_id = self.capif_api_details["registered_security_contexes"][i]["api_id"]
            aef_id = self.capif_api_details["registered_security_contexes"][i]["aef_id"]

            security_info = {
                "prefSecurityMethods": ["Oauth"],
                "authenticationInfo": "string",
                "authorizationInfo": "string",
                "aefId": aef_id,
                "apiId": api_id
            }

            payload["securityInfo"].append(security_info)

        try:
            response = requests.put(url,
                                    json=payload,
                                    cert=(self.signed_key_crt_path,
                                          self.private_key_path),
                                    verify=self.ca_root_path
                                    )
            response.raise_for_status()
            self.logger.info("Security service properly registered")
        except requests.RequestException as e:
            self.logger.error(
                "Error when registering the security service: %s", str(e))
            raise

    def __get_security_token(self):
        """
        :param api_name: El nombre del API devuelto por descubrir servicios
        :param aef_id: El aef_id relevante devuelto por descubrir servicios
        :return: El token de acceso (jwt)
        """
        url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/securities/{self.capif_api_details['api_invoker_id']}/token"
        # Construir el scope concatenando aef_id y api_name separados por un ';'
        scope_parts = []

        # Iterar sobre los contextos registrados y construir las partes del scope
        for context in self.capif_api_details["registered_security_contexes"]:
            aef_id = context["aef_id"]
            api_name = context["api_name"]
            scope_parts.append(f"{aef_id}:{api_name}")

        # Unir todas las partes del scope con ';' y añadir el prefijo '3gpp#'
        scope = "3gpp#" + ";".join(scope_parts)

        payload = {
            "grant_type": "client_credentials",
            "client_id": self.capif_api_details["api_invoker_id"],
            "client_secret": "string",
            "scope": scope
        }
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
        }

        try:
            response = requests.post(url,
                                     headers=headers,
                                     data=payload,
                                     cert=(self.signed_key_crt_path,
                                           self.private_key_path),
                                     verify=self.ca_root_path
                                     )
            response.raise_for_status()
            response_payload = response.json()
            self.logger.info("Security token successfully obtained")
            return response_payload
        except requests.RequestException as e:
            self.logger.error(
                "Error obtaining the security token: %s ", str(e))
            raise

    def discover_service_apis(self):
        """
        Descubre los APIs de servicio desde CAPIF con filtros basados en un archivo JSON.
        :return: Payload JSON con los detalles de los APIs de servicio
        """
        # Cargar los parámetros desde el archivo JSON

        # Filtrar parámetros que no sean vacíos "
        filters = self.discover_filter

        query_params = {k: v for k, v in filters.items() if v.strip()}

        # Formar la URL con los parámetros de query
        query_string = "&".join([f"{k}={v}" for k, v in query_params.items()])

        url = f"https://{self.capif_host}:{self.capif_https_port}/{self.capif_api_details['discover_services_url']}{self.capif_api_details['api_invoker_id']}"

        if query_string:
            url += f"&{query_string}"

        try:
            response = requests.get(
                url,
                headers={"Content-Type": "application/json"},
                cert=(self.signed_key_crt_path, self.private_key_path),
                verify=self.ca_root_path
            )

            response.raise_for_status()
            response_payload = response.json()
            self.logger.info("Service APIs successfully discovered")
            return response_payload
        except requests.RequestException as e:
            self.logger.error("Error discovering service APIs: %s", str(e))
            raise

    def retrieve_api_description_by_name(self, api_name):
        """
        Recupera la descripción del API por nombre.
        :param api_name: Nombre del API
        :return: Descripción del API
        """
        self.logger.info(
            "Retrieving the API description for api_name=%s", api_name)
        capif_apifs = self.discover_service_apis()
        endpoints = [api for api in capif_apifs["serviceAPIDescriptions"]
                     if api["apiName"] == api_name]
        if not endpoints:
            error_message = (
                f"Could not find available endpoints for api_name: {api_name}. "
                "Make sure that a) your Invoker is registered and onboarded to CAPIF and "
                "b) the NEF emulator has been registered and onboarded to CAPIF"
            )
            self.logger.error(error_message)
            raise ServiceDiscoverer.ServiceDiscovererException(error_message)
        else:
            self.logger.info("API description successfully retrieved")
            return endpoints[0]

    def retrieve_specific_resource_name(self, api_name, resource_name):
        """
        Recupera la URL para recursos específicos dentro de los APIs.
        :param api_name: Nombre del API
        :param resource_name: Nombre del recurso
        :return: URL del recurso específico
        """
        self.logger.info(
            "Retrieving the URL for resource_name=%s in api_name=%s", resource_name, api_name)
        api_description = self.retrieve_api_description_by_name(api_name)
        version_dictionary = api_description["aefProfiles"][0]["versions"][0]
        version = version_dictionary["apiVersion"]
        resources = version_dictionary["resources"]
        uris = [resource["uri"]
                for resource in resources if resource["resourceName"] == resource_name]

        if not uris:
            error_message = f"Could not find resource_name: {resource_name} at api_name {api_name}"
            self.logger.error(error_message)
            raise ServiceDiscoverer.ServiceDiscovererException(error_message)
        else:
            uri = uris[0]
            if not uri.startswith("/"):
                uri = "/" + uri
            if api_name.endswith("/"):
                api_name = api_name[:-1]
            result_url = api_name + "/" + version + uri
            self.logger.info(
                "URL of the specific resource successfully retrieved: %s", result_url)
            return result_url

    def save_security_token(self, token):
        self.capif_api_details["access_token"] = token
        self.__cache_security_context()

    def get_tokens(self):

        self.get_security_context()
        token = self.get_access_token()
        self.save_security_token(token)

    def discover(self):
        endpoints = self.discover_service_apis()

        if len(endpoints) > 0:
            self.save_api_discovered(endpoints)
        else:
            self.logger.error(
                "No endpoints have been registered. Make sure a Provider has Published an API to CAPIF first")

    def save_api_discovered(self, endpoints):
        self.capif_api_details["registered_security_contexes"] = []
        for service in endpoints["serviceAPIDescriptions"]:
            api_name = service["apiName"]
            api_id = service["apiId"]
            for n in service["aefProfiles"]:
                aef_id = n["aefId"]
                self.capif_api_details["registered_security_contexes"].append(
                    {"api_name": api_name, "api_id": api_id, "aef_id": aef_id})
        self.save_api_details()

    def save_api_details(self):
        try:
            # Define the path to save the details
            file_path = os.path.join(
                self.invoker_folder, "capif_api_security_context_details-" + self.capif_invoker_username + ".json")

            # Save the details as a JSON file
            with open(file_path, "w") as outfile:
                json.dump(self.capif_api_details, outfile, indent=4)

            # Log the success of the operation
            self.logger.info("API provider details correctly saved")

        except Exception as e:
            # Log any errors that occur during the save process
            self.logger.error(
                "Error while saving API provider details: %s", str(e))
            raise
