# Base sur le package de preinstallation
# Par Brigitte Bigi et l'equipe SPPAS

__author__ = "Roland Trouville, Brigitte Bigi"
__copyright__ = "Copyright 2015+, Consortium MonPaGe"
__license__ = "Creative Commons 4.0 By-Nc-Sa"
__maintainer__ = "Roland Trouville"
__email__ = "contact.monpage@gmail.com"

__status__ = "Production"

import importlib.util
import logging
import subprocess
import sys
from importlib.metadata import version
from typing import Optional

logger = logging.getLogger(__name__)


class Package:
	"""
	A class representing a Python package with its pip name, import name, and version.
	This class exists because a package can have different names when importing and installing
	Example:\
		Pillow has to be installed via `pip install pillow`, but imported with `import PIL`

	Saving just *one* of them wouldn't allow to check/install the package

	This class stores information about a Python package, including:
	- The name used to install it via pip when doing `pip install [pip_name]`
	- The name used to import it in code when doing `import [import_name]`
	- The version specification (version)

	Attributes:
		pip_name (str): The name of the package used for pip installation
		import_name (str): The name used when importing the package
		version (Optional[str]): The version requirement, or None for latest version
	"""

	def __init__(self, pip_name: str, import_name: str, version: Optional[str]):
		self.pip_name = pip_name
		self.import_name = import_name
		self.version = version


class PackageManager:
	"""
	A class for managing Python package dependencies.

	This class provides functionality to check for required packages,
	install missing packages, and verify package installation status.
	It includes methods for:
	- Setting OS-specific package requirements
	- Checking if required packages are installed
	- Installing missing packages
	- Verifying all required packages are present

	The package requirements are stored in a dictionary with package names
	and their required versions. The class handles package management
	across Windows, Mac and other operating systems.
	"""

	@staticmethod
	def package_is_installed(package_name: str) -> bool:
		"""
		Check if a Python package is installed.

		Args:
			package_name (str): The name of the package to check

		This method uses importlib.util.find_spec() to check if a package is installed
		in the current Python environment. True if the package needs to be installed via pip, False otherwise
		"""

		return importlib.util.find_spec(package_name) is not None

	@staticmethod
	def _run_pip_command(args: list):
		"""
		Run a pip command with the specified arguments.

		Args:
			args (list): List of arguments to pass to pip (e.g., ['install', 'requests==2.28.0', '--no-cache-dir'])

		Raises:
			subprocess.CalledProcessError: If the pip command fails
		"""

		pip_args = [
			sys.executable,
			"-m",
			"pip",
		] + args

		logger.info("Launching PIP subprocess: " + " ".join(pip_args))
		subprocess.check_call(pip_args)

	@staticmethod
	def install_package(package_name: str, version: Optional[str]):
		"""
		Install a Python package using pip.

		Args:
			package_name (str): The name of the package to install
			version (Optional[str]): The specific version to install, or None for latest version

		This method uses pip via subprocess to install the specified package. If a version
		is provided, it will install that specific version, otherwise it will install the
		latest available version. The installation is performed with cache disabled and
		pip version check disabled for faster installation.

		Raises:
			subprocess.CalledProcessError: If the pip install command fails
		"""

		logger.info(f"Installing '{package_name}' of version {version}")

		print("Tentative d'installation de `" + package_name + "`:")
		requirement_specifier = package_name

		if version is not None:
			requirement_specifier += "==" + version

		PackageManager._run_pip_command(
			[
				"install",
				requirement_specifier,
				"--no-cache-dir",
				"--disable-pip-version-check",
			]
		)

	@staticmethod
	def uninstall_package(package_name: str):
		"""
		Uninstall a Python package using pip.

		Args:
			package_name (str): The name of the package to uninstall

		This method uses pip via subprocess to uninstall the specified package.
		The uninstallation is performed without confirmation prompts.

		Raises:
			subprocess.CalledProcessError: If the pip uninstall command fails
		"""

		logger.info(f"Uninstalling '{package_name}'")
		PackageManager._run_pip_command(
			["uninstall", package_name, "-y"]
		)  # Auto-confirm uninstallation

	@staticmethod
	def incorrect_package_version_is_installed(package: Package) -> bool:
		if package.version:
			# Get package version
			installed_package_version: str = version(package.import_name)

			if installed_package_version != package.version:
				return True

		return False

	def install_packages(self):
		"""
		Install required Python packages for the application.

		This method checks for missing packages from the requirements and installs them
		using pip if necessary. It:
		1. Iterates through the required packages dictionary
		2. Checks if the package needs to be installed
		3. Installs the missing package with specified version (or latest)
		4. Prints a message if all required packages are already installed

		The installation process uses pip with caching and version checking disabled
		for faster installation.
		"""

		logger.info("Installing packages")

		from tools.environment_tools import EnvironmentTools

		installed_any_package = False
		for package_name in EnvironmentTools.required_packages.keys():
			logger.info(f"Checking {package_name}")
			package: Package = EnvironmentTools.required_packages[package_name]

			if self.package_is_installed(package.import_name):
				if PackageManager.incorrect_package_version_is_installed(package):
					# Reinstall the package using correct version
					PackageManager.uninstall_package(package.import_name)
					PackageManager.install_package(package.import_name, package.version)
			else:
				logger.info(f"'{package_name}' should be installed")

				try:
					PackageManager.install_package(package.pip_name, package.version)
				except subprocess.CalledProcessError as e:
					logger.exception(
						f"Error installing '{package.pip_name}' of version {package.version}: {e}"
					)

					if (sys.version_info.major, sys.version_info.minor) == (3, 14):
						(minimum_recommended, maximum_recommended) = (
							EnvironmentTools.get_recommended_python_version_range()
						)
						print(
							f"Vous utilisez la version de Python 3.14, si MonPage ne fonctionne pas, essayez d'utiliser la version de Python entre {minimum_recommended} et {maximum_recommended}"
						)

					exit()

				installed_any_package = True

		if not installed_any_package:
			logger.info("All required packages are already installed")
			print(
				"Toutes les librairies requises sont déja présentes dans "
				+ sys.executable
			)

	@staticmethod
	def check_all_packages_installed() -> bool:
		"""
		Check if all required packages are installed.

		This method iterates through the required packages dictionary and checks
		if each package is installed in the current Python environment. If any
		package is missing, it prints a message indicating which package needs
		to be installed.

		Returns:
			bool: True if all required packages are installed, False if any package is missing
		"""

		from tools.environment_tools import EnvironmentTools

		any_package_missing = False
		for package_name in EnvironmentTools.required_packages.keys():
			package = EnvironmentTools.required_packages[package_name]
			if PackageManager.package_is_installed(package.import_name):
				if PackageManager.incorrect_package_version_is_installed(package):
					any_package_missing = True
			else:
				print(
					"il vous manque une des librairies requises pour l'applicatif ("
					+ package_name
					+ ")"
				)
				any_package_missing = True

		return not any_package_missing
