import os
from enum import Enum
from functools import cached_property
from pathlib import Path
from typing import Self

from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.networking import enrich_iface_types, list_interfaces
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr


class CpuVendor(Enum):
	AuthenticAMD = 'amd'
	GenuineIntel = 'intel'
	_Unknown = 'unknown'

	@classmethod
	def get_vendor(cls, name: str) -> Self:
		if vendor := getattr(cls, name, None):
			return vendor
		else:
			debug(f"Unknown CPU vendor '{name}' detected.")
			return cls._Unknown

	def _has_microcode(self) -> bool:
		match self:
			case CpuVendor.AuthenticAMD | CpuVendor.GenuineIntel:
				return True
			case _:
				return False

	def get_ucode(self) -> Path | None:
		if self._has_microcode():
			return Path(self.value + '-ucode.img')
		return None


class GfxPackage(Enum):
	Dkms = 'dkms'
	IntelMediaDriver = 'intel-media-driver'
	LibvaIntelDriver = 'libva-intel-driver'
	LibvaNvidiaDriver = 'libva-nvidia-driver'
	Mesa = 'mesa'
	NvidiaOpenDkms = 'nvidia-open-dkms'
	VulkanIntel = 'vulkan-intel'
	VulkanRadeon = 'vulkan-radeon'
	VulkanNouveau = 'vulkan-nouveau'
	Xf86VideoAmdgpu = 'xf86-video-amdgpu'
	Xf86VideoAti = 'xf86-video-ati'
	Xf86VideoNouveau = 'xf86-video-nouveau'
	XorgServer = 'xorg-server'
	XorgXinit = 'xorg-xinit'


class GfxDriver(Enum):
	AllOpenSource = 'All open-source'
	AmdOpenSource = 'AMD / ATI (open-source)'
	IntelOpenSource = 'Intel (open-source)'
	NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)'
	NvidiaOpenSource = 'Nvidia (open-source nouveau driver)'
	VMOpenSource = 'VirtualBox (open-source)'

	def is_nvidia(self) -> bool:
		match self:
			case GfxDriver.NvidiaOpenSource | GfxDriver.NvidiaOpenKernel:
				return True
			case _:
				return False

	def packages_text(self) -> str:
		pkg_names = [p.value for p in self.gfx_packages()]
		text = tr('Installed packages') + ':\n'

		for p in sorted(pkg_names):
			text += f'\t- {p}\n'

		return text

	def gfx_packages(self) -> list[GfxPackage]:
		packages = [GfxPackage.XorgServer, GfxPackage.XorgXinit]

		match self:
			case GfxDriver.AllOpenSource:
				packages += [
					GfxPackage.Mesa,
					GfxPackage.Xf86VideoAmdgpu,
					GfxPackage.Xf86VideoAti,
					GfxPackage.Xf86VideoNouveau,
					GfxPackage.LibvaIntelDriver,
					GfxPackage.IntelMediaDriver,
					GfxPackage.VulkanRadeon,
					GfxPackage.VulkanIntel,
					GfxPackage.VulkanNouveau,
				]
			case GfxDriver.AmdOpenSource:
				packages += [
					GfxPackage.Mesa,
					GfxPackage.Xf86VideoAmdgpu,
					GfxPackage.Xf86VideoAti,
					GfxPackage.VulkanRadeon,
				]
			case GfxDriver.IntelOpenSource:
				packages += [
					GfxPackage.Mesa,
					GfxPackage.LibvaIntelDriver,
					GfxPackage.IntelMediaDriver,
					GfxPackage.VulkanIntel,
				]
			case GfxDriver.NvidiaOpenKernel:
				packages += [
					GfxPackage.NvidiaOpenDkms,
					GfxPackage.Dkms,
					GfxPackage.LibvaNvidiaDriver,
				]
			case GfxDriver.NvidiaOpenSource:
				packages += [
					GfxPackage.Mesa,
					GfxPackage.Xf86VideoNouveau,
					GfxPackage.VulkanNouveau,
				]
			case GfxDriver.VMOpenSource:
				packages += [
					GfxPackage.Mesa,
				]

		return packages


class _SysInfo:
	def __init__(self) -> None:
		pass

	@cached_property
	def has_battery(self) -> bool:
		for type_path in Path('/sys/class/power_supply/').glob('*/type'):
			try:
				with open(type_path) as f:
					if f.read().strip() == 'Battery':
						return True
			except OSError:
				continue

		return False

	@cached_property
	def cpu_info(self) -> dict[str, str]:
		"""
		Returns system cpu information
		"""
		cpu_info_path = Path('/proc/cpuinfo')
		cpu: dict[str, str] = {}

		with cpu_info_path.open() as file:
			for line in file:
				if line := line.strip():
					key, value = line.split(':', maxsplit=1)
					cpu[key.strip()] = value.strip()

		return cpu

	@cached_property
	def mem_info(self) -> dict[str, int]:
		"""
		Returns system memory information
		"""
		mem_info_path = Path('/proc/meminfo')
		mem_info: dict[str, int] = {}

		with mem_info_path.open() as file:
			for line in file:
				key, value = line.strip().split(':')
				num = value.split()[0]
				mem_info[key] = int(num)

		return mem_info

	def mem_info_by_key(self, key: str) -> int:
		return self.mem_info[key]

	@cached_property
	def loaded_modules(self) -> list[str]:
		"""
		Returns loaded kernel modules
		"""
		modules_path = Path('/proc/modules')
		modules: list[str] = []

		with modules_path.open() as file:
			for line in file:
				module = line.split(maxsplit=1)[0]
				modules.append(module)

		return modules

	@cached_property
	def graphics_devices(self) -> dict[str, str]:
		"""
		Returns detected graphics devices (cached)
		"""
		cards: dict[str, str] = {}
		for line in SysCommand('lspci'):
			if b' VGA ' in line or b' 3D ' in line:
				_, identifier = line.split(b': ', 1)
				cards[identifier.strip().decode('UTF-8')] = str(line)
		return cards


_sys_info = _SysInfo()


class SysInfo:
	@staticmethod
	def has_battery() -> bool:
		return _sys_info.has_battery

	@staticmethod
	def has_wifi() -> bool:
		ifaces = list(list_interfaces().values())
		return 'WIRELESS' in enrich_iface_types(ifaces).values()

	@staticmethod
	def has_uefi() -> bool:
		return os.path.isdir('/sys/firmware/efi')

	@staticmethod
	def _graphics_devices() -> dict[str, str]:
		return _sys_info.graphics_devices

	@staticmethod
	def has_nvidia_graphics() -> bool:
		return any('nvidia' in x.lower() for x in _sys_info.graphics_devices)

	@staticmethod
	def has_amd_graphics() -> bool:
		return any('amd' in x.lower() for x in _sys_info.graphics_devices)

	@staticmethod
	def has_intel_graphics() -> bool:
		return any('intel' in x.lower() for x in _sys_info.graphics_devices)

	@staticmethod
	def cpu_vendor() -> CpuVendor | None:
		if vendor := _sys_info.cpu_info.get('vendor_id'):
			return CpuVendor.get_vendor(vendor)
		return None

	@staticmethod
	def cpu_model() -> str | None:
		return _sys_info.cpu_info.get('model name', None)

	@staticmethod
	def sys_vendor() -> str | None:
		try:
			with open('/sys/devices/virtual/dmi/id/sys_vendor') as vendor:
				return vendor.read().strip()
		except FileNotFoundError:
			return None

	@staticmethod
	def product_name() -> str | None:
		try:
			with open('/sys/devices/virtual/dmi/id/product_name') as product:
				return product.read().strip()
		except FileNotFoundError:
			return None

	@staticmethod
	def mem_available() -> int:
		return _sys_info.mem_info_by_key('MemAvailable')

	@staticmethod
	def mem_free() -> int:
		return _sys_info.mem_info_by_key('MemFree')

	@staticmethod
	def mem_total() -> int:
		return _sys_info.mem_info_by_key('MemTotal')

	@staticmethod
	def virtualization() -> str | None:
		try:
			return str(SysCommand('systemd-detect-virt')).strip('\r\n')
		except SysCallError as err:
			debug(f'Could not detect virtual system: {err}')

		return None

	@staticmethod
	def is_vm() -> bool:
		try:
			result = SysCommand('systemd-detect-virt')
			return b'none' not in b''.join(result).lower()
		except SysCallError as err:
			debug(f'System is not running in a VM: {err}')

		return False

	@staticmethod
	def requires_sof_fw() -> bool:
		return 'snd_sof' in _sys_info.loaded_modules

	@staticmethod
	def requires_alsa_fw() -> bool:
		modules = (
			'snd_asihpi',
			'snd_cs46xx',
			'snd_darla20',
			'snd_darla24',
			'snd_echo3g',
			'snd_emu10k1',
			'snd_gina20',
			'snd_gina24',
			'snd_hda_codec_ca0132',
			'snd_hdsp',
			'snd_indigo',
			'snd_indigodj',
			'snd_indigodjx',
			'snd_indigoio',
			'snd_indigoiox',
			'snd_layla20',
			'snd_layla24',
			'snd_mia',
			'snd_mixart',
			'snd_mona',
			'snd_pcxhr',
			'snd_vx_lib',
		)

		for loaded_module in _sys_info.loaded_modules:
			if loaded_module in modules:
				return True

		return False
