import logging
import os
import platform
import sys
from typing import Optional, Tuple

from tools.package_installer import Package

"""
Classe permettant de gérer l'environnement et les packages requis pour python et de
les installer automatiquement si ils sont manquants.
Nécessite la présence d'un fichier environment.yaml et d'un fichier packages.yaml à la racine du projet.
Deux fichiers d'exemples sont disponibles dans le toolkit
La liste des packages requis se défini dans le fichier packages.yaml à la racine du projet, sous un forme de ce genre:

numpy:
  import_name: numpy
  version: null
pyqt5:
  import_name: PyQt5
  version: null

La clef est le nom du package tel que utilisé pour l'installer via pip
Le import_name est le nom du package tel que trouvé par importlib (le nom utilisé dans la commande import)
la version permet de spécifier un numéro de version précis

Dépendances : pyaml, mais il sera installé automatiquement
"""

__author__ = "Roland Trouville"
__copyright__ = "Copyright 2025, Laboratoire de Phonetique et Phonologie - CNRS"
__credits__ = ["Rob Knight", "Peter Maxwell", "Gavin Huttley", "Matthew Wakefield"]
__license__ = "Apache 2"
__version__ = "1.0"
__maintainer__ = "Roland Trouville"
__email__ = "roland.trouville@sorbonne-nouvelle.fr"
__status__ = "Production"

logger = logging.getLogger(__name__)


class EnvironmentTools:
	"""
	Utility class for checking and configuring the Python execution environment.

	This class provides methods to verify the operating system, Python version,
	virtual environment status, and required packages for the project.
	It loads configuration parameters from YAML files and manages package requirements.

	Attributes:
	    parameters (dict[str, str]): Configuration parameters loaded from environment.yaml.
	    required_packages (dict[str, Package]): Dictionary of required Python packages.
	    CONFIG_ROOT_PATH (str): Root directory for configuration files.
	    PACKAGES_CONFIG_PATH (str): Path to the packages configuration YAML file.
	    ENVIRONMENT_CONFIG_PATH (str): Path to the environment configuration YAML file.
	"""

	parameters: dict[str, str] = dict()
	required_packages: dict[str, Package] = dict()

	current_python_version: str = f"{sys.version_info.major}.{sys.version_info.minor}"
	required_python_major_version: int
	minimum_python_minor_version: int
	recommended_python_minor_version: int

	_loaded: bool = False

	CONFIG_ROOT_PATH = "config"
	PACKAGES_CONFIG_PATH = os.path.join(CONFIG_ROOT_PATH, "packages.yaml")
	ENVIRONMENT_CONFIG_PATH = os.path.join(CONFIG_ROOT_PATH, "environment.yaml")

	@staticmethod
	def get_recommended_python_version_range() -> Tuple[str, str]:
		if not EnvironmentTools._loaded:
			EnvironmentTools.check_environment()

		minimum_recommended = f"{EnvironmentTools.required_python_major_version}.{EnvironmentTools.minimum_python_minor_version}"
		maximum_recommended = f"{EnvironmentTools.required_python_major_version}.{EnvironmentTools.recommended_python_minor_version}"

		return (minimum_recommended, maximum_recommended)

	@staticmethod
	def check_environment():
		"""
		Vérifie que l'environnement d'exécution est correctement configuré.

		Cette méthode effectue les vérifications suivantes:
		- Détection du système d'exploitation
		- Vérification de l'utilisation d'un environnement virtuel
		- Vérification de la version de Python utilisée
		- Vérification du nom de l'environnement virtuel
		- Chargement des packages requis depuis les fichiers de configuration

		La méthode termine le programme avec exit() si l'une des conditions n'est pas remplie.
		"""

		EnvironmentTools._loaded = True

		logger.info("Checking environment")

		os = EnvironmentTools.get_current_os()

		if os is None:
			logger.error(
				"Impossible de reconnaitre le systeme d'exploitation - fermeture du programme"
			)
			exit()

		logger.info(f"OS detected: {os}")

		venv = EnvironmentTools.__get_venv_base_path()

		if venv is None:
			print(
				"Script lancé sans environnement virtuel, assurez vous d'en créer un avant de lancer le programme"
			)
			exit()

		logger.info(f"Venv path: {venv}")

		print(
			"La version de Python utilisée est : "
			+ EnvironmentTools.current_python_version
		)
		logger.info(
			f"Current Python version: {EnvironmentTools.current_python_version}"
		)

		from sqlite3 import sqlite_version

		print("La version de SQLite utilisée au runtime est : " + sqlite_version)
		logger.info(f"SQLite version used: {sqlite_version}")

		try:
			EnvironmentTools.__load_parameters()
		except FileNotFoundError as e:
			print("\n".join(e.args))  # Show errors

		try:
			logger.info("Trying to read the required Python major version")
			EnvironmentTools.required_python_major_version = int(
				EnvironmentTools.parameters["required_python_major_version"]
			)
		except ValueError:
			logger.error("Invalid required Python major version")
			print("La version majeure de Python requise est invalide")
			exit()
		except (KeyError, TypeError):
			logger.error("Missing required Python major version")
			print("La version majeure de Python requise est manquante")
			exit()

		if sys.version_info.major != EnvironmentTools.required_python_major_version:
			logger.error(
				f"Current Python's major version doesn't match the required ({EnvironmentTools.required_python_major_version})"
			)
			print(
				f"Il faut utiliser Python {EnvironmentTools.required_python_major_version}.X"
			)
			exit()

		try:
			logger.info("Trying to read the required Python minor version")
			EnvironmentTools.minimum_python_minor_version = int(
				EnvironmentTools.parameters["minimum_python_minor_version"]
			)
		except ValueError:
			logger.error("Invalid required Python minor version")
			print("La version mineure de Python requise est invalide")
			exit()
		except (KeyError, TypeError):
			logger.error("Missing required Python minor version")
			print("La version mineure de Python requise est manquante")
			exit()

		if sys.version_info.minor < EnvironmentTools.minimum_python_minor_version:
			logger.error(
				f"Current Python's minor version doesn't match the required ({EnvironmentTools.minimum_python_minor_version})"
			)
			print(
				f"Il faut utiliser Python >={EnvironmentTools.required_python_major_version}.{EnvironmentTools.minimum_python_minor_version}"
			)
			exit()

		try:
			logger.info("Trying to read the required Python minor version")
			EnvironmentTools.recommended_python_minor_version = int(
				EnvironmentTools.parameters["recommended_python_minor_version"]
			)
		except ValueError:
			logger.warning("Invalid recommended Python minor version")
			print("La version recommandee de Python requise est invalide")
		except (KeyError, TypeError):
			logger.warning("Missing required Python minor version")
			print("La version recommandee de Python requise est manquante")

		try:
			logger.info("Trying to read 'venv_name'")
			venv_name = EnvironmentTools.parameters["venv_name"]
		except ValueError:
			logger.error("Invalid venv name")
			print("La racine du chemin vers l'environnement virtuel est invalide")
			exit()
		except (KeyError, TypeError):
			logger.error("Missing venv name")
			print("La racine du chemin vers l'environnement virtuel est manquante")
			exit()

		if venv_name != venv:
			logger.warning(f"Unexpected venv name {venv_name}, expected {venv}")
			print(
				f"WARNING - Script lancé dans un environnement virtuel non attendu ({venv} au lieu de {venv_name}), êtes vous sur ?"
			)

		EnvironmentTools.__load_packages_from_file(
			EnvironmentTools.PACKAGES_CONFIG_PATH
		)
		# Try to override the loaded packages
		try:
			logger.info("Trying to override settings")
			EnvironmentTools.__load_packages_from_file(
				f"config/packages-{sys.version_info.major}-{sys.version_info.minor}.yaml"
			)
		except FileNotFoundError:
			logger.info("No override config file, nothing to override")
			# No override config file, nothing to override
			pass

		logger.info(
			f"Launched in venv {venv} with python {EnvironmentTools.current_python_version} on {os}"
		)
		print(
			"Script lancé sur",
			os,
			"avec python",
			EnvironmentTools.current_python_version,
			"dans env",
			venv,
		)

	@staticmethod
	def __get_project_root_path() -> str:
		"""
		Determine the root directory path of the project based on the module's depth.

		This method calculates the project root by traversing up the directory hierarchy
		relative to the current file's location. The number of directory levels moved up
		corresponds to the number of components in the module's dotted path.

		Returns:
			str: The absolute path to the root directory of the project.
		"""
		module = __name__
		module_depth = len(module.split("."))
		current_file_path = __file__
		project_root_path = current_file_path

		for _ in range(module_depth):
			project_root_path = os.path.join(project_root_path, "..")  # Move backwards

		return project_root_path

	@staticmethod
	def __load_packages_from_file(relative_path: str):
		"""
		Charge les informations des packages à partir d'un fichier YAML.
		Le fichier doit exister sinon une exception FileNotFoundError est levée.
		Le fichier YAML doit contenir une section 'globals' pour les packages globaux
		et peut contenir des sections spécifiques au système d'exploitation (windows, linux, mac).

		Parameters:
						relative_path (str): Chemin relatif vers le fichier YAML contenant les informations des packages depuis la racine du projet.
		"""

		path = os.path.normpath(
			os.path.join(EnvironmentTools.__get_project_root_path(), relative_path)
		)
		logger.info(f"Loading packages from {path}")
		if not os.path.isfile(path):
			logger.warning(f"File {path} not found")
			raise FileNotFoundError(f"Fichier {path} introuvable")

		print(f"Chargement de {path}")

		EnvironmentTools.__check_pyyaml()
		import yaml

		raw_format: dict[str, dict[str, dict[str, str]]] = dict()

		logger.info(f"Opening {path}")
		with open(path, "r") as f:
			raw_format = yaml.load(f, Loader=yaml.FullLoader)
		logger.info(f"{path} was read successfully")

		global_packages: dict[str, dict[str, str]] = dict()

		try:
			logger.info("Getting 'globals' key from the config file")
			global_packages = raw_format["globals"]
		except KeyError:
			logger.warning("Key 'globals' not found in the config file")
			pass

		EnvironmentTools.__set_packages_from_dict(global_packages)

		os_specific_packages: dict[str, dict[str, str]] = dict()
		os_name = EnvironmentTools.get_current_os()

		try:
			if os_name is not None:
				logger.info(f"Trying to get os ({os_name}) specific settings")
				os_specific_packages = raw_format[os_name.lower()]
		except KeyError:
			logger.info(f"Os ({os_name}) specific settings weren't found")
			pass

		EnvironmentTools.__set_packages_from_dict(os_specific_packages)

	@staticmethod
	def __set_packages_from_dict(dic: dict[str, dict[str, str]]):
		"""
		Met à jour le dictionnaire des packages requis avec les informations du dictionnaire fourni.

		Le dictionnaire d'entrée doit contenir des clés correspondant aux noms pip des packages,
		et des valeurs contenant des dictionnaires avec les clés 'version' et/ou 'import_name'.
		Si la version est définie à 'remove', le package sera supprimé de la liste des packages requis.
		Si l'import_name n'est pas spécifié, le nom pip est utilisé comme nom d'importation.

		Parameters:
			dic (dict[str, dict[str, str]]): Dictionnaire contenant les informations des packages à ajouter.
		"""
		logger.info("Setting environment dependencies from dictionary")
		for pip_name in dic.keys():
			logger.debug(f"Processing package {pip_name}")
			package_info: dict[str, str] = dic[pip_name]

			if pip_name not in EnvironmentTools.required_packages.keys():
				version: Optional[str] = None
				import_name: str = pip_name  # By default, the package's pip name is used as the import name
			else:
				# Reuse existing values
				logger.info(f"Need to override {pip_name} info")
				version = EnvironmentTools.required_packages[pip_name].version
				import_name = EnvironmentTools.required_packages[pip_name].import_name

			if package_info is not None:
				try:
					version = package_info["version"]
				except KeyError:
					pass

				if version == "remove":
					logger.info(f"'remove' version is set, removing package {pip_name}")
					# The package shouldn't be installed
					if pip_name in EnvironmentTools.required_packages.keys():
						# Need to remove the package, since it was already added
						EnvironmentTools.required_packages.pop(pip_name)

					# Ignore the package, don't add it to the dictionary
					continue

				try:
					import_name = package_info["import_name"]
				except KeyError:
					pass

			logger.info(f"Using version '{version}'")
			logger.info(f"import name '{import_name}' is set")

			EnvironmentTools.required_packages[pip_name] = Package(
				pip_name, import_name, version
			)

	@staticmethod
	def __load_parameters():
		"""
		Load configuration parameters from a YAML file specified by ENVIRONMENT_CONFIG_PATH.

		The method checks if the configuration file exists, verifies that the 'pyyaml'
		library is installed (installing it if necessary), and then reads the YAML file
		contents into the `EnvironmentTools.parameters` dictionary.

		Raises:
			FileNotFoundError: If the configuration file does not exist.
		"""
		logger.info(
			f"Loading config parameters from {EnvironmentTools.ENVIRONMENT_CONFIG_PATH}"
		)

		if not os.path.isfile(EnvironmentTools.ENVIRONMENT_CONFIG_PATH):
			logger.error(
				f"Config file {EnvironmentTools.ENVIRONMENT_CONFIG_PATH} not found"
			)
			raise FileNotFoundError(
				f"Fichier {EnvironmentTools.ENVIRONMENT_CONFIG_PATH} introuvable"
			)

		EnvironmentTools.__check_pyyaml()
		import yaml

		logger.info(f"Opening config file {EnvironmentTools.ENVIRONMENT_CONFIG_PATH}")
		with open(EnvironmentTools.ENVIRONMENT_CONFIG_PATH, "r") as f:
			logger.info(
				f"Reading config file {EnvironmentTools.ENVIRONMENT_CONFIG_PATH}"
			)
			EnvironmentTools.parameters = yaml.load(f, Loader=yaml.FullLoader)
		logger.info("Environment parameters are loaded")

		# if not os.path.isfile("packages.yaml"):
		# 	raise FileNotFoundError("Fichier packages.yaml introuvable")
		# f = open("packages.yaml", "r")
		# EnvironmentTools.required_packages = yaml.load(f, Loader=yaml.FullLoader)
		# f.close()

	@staticmethod
	def __get_venv_base_path():
		"""
		Determine the base path name of the active Python virtual environment, if any.

		Compares `sys.prefix` with `sys.base_prefix` to detect if the current interpreter
		is running inside a virtual environment. If so, returns the name of the virtual
		environment directory; otherwise, returns None.

		Returns:
			str | None: The base directory name of the virtual environment, or None if no
			virtual environment is active.
		"""
		logger.info("Getting venv base path")

		venv = sys.prefix
		if venv != sys.base_prefix:
			logger.info("Base prefix differs, venv detected")
			path_components = venv.split(os.sep)
			return path_components[-1]

		logger.warning(f"{venv} is the same as {sys.base_prefix}, no venv")
		return None

	@staticmethod
	def get_current_os():
		"""
		Identify the current operating system.

		Uses `platform.platform()` to detect if the OS is Windows, Linux, or macOS.
		If the OS cannot be identified among these, returns None.

		Returns:
			str | None: One of "Windows", "Linux", "Mac", or None if the OS is unknown.
		"""
		logger.info("Getting current OS")

		tmp = platform.platform(aliased=True)
		if "Windows" in tmp:
			return "Windows"
		if "Linux" in tmp:
			return "Linux"
		if "macOS" in tmp:
			return "Mac"

		logger.warning("Unknown OS")
		return None

	@staticmethod
	def __check_pyyaml():
		"""
		Verify that the 'pyyaml' package is installed.

		Uses PackageManager to check for the presence of 'pyyaml' and installs it if missing.
		Logs the installation status accordingly.
		"""
		logger.info("Checking 'pyyaml' library presence")
		from tools.package_installer import PackageManager

		if not PackageManager.package_is_installed("yaml"):
			logger.info("Installing 'pyyaml' library")
			PackageManager.install_package("pyyaml", None)

		logger.info("'pyyaml' library is present, don't install")
