# -*- coding: utf-8 -*-
"""
Cargador dinámico de DLL para basskit.

Gestiona la carga de bass.dll y todos los add-ons de forma segura,
validando plataforma, arquitectura y versión de Python.
"""

import ctypes
import platform
import struct
import sys
from pathlib import Path
from typing import Optional

from .excepciones import (
	ErrorArquitecturaInvalida,
	ErrorDLLNoEncontrada,
	ErrorVersionPython,
)
from .rutas import obtener_directorio_dlls, registrar_directorio_dlls


# ─── Caché global de DLL cargadas ────────────────────────────────────
_dlls_cargadas: dict[str, ctypes.WinDLL] = {}
_directorio_registrado: bool = False


def validar_plataforma() -> None:
	"""
	Valida que el sistema sea Windows 64 bits con Python 3.13+.

	Lanza:
		ErrorArquitecturaInvalida: Si no es Windows 64 bits.
		ErrorVersionPython: Si Python es anterior a 3.13.
	"""
	# Verificar sistema operativo
	if platform.system() != "Windows":
		raise ErrorArquitecturaInvalida(
			f"{platform.system()} {platform.machine()}"
		)

	# Verificar arquitectura de Python (no del OS)
	bits = struct.calcsize("P") * 8
	if bits != 64:
		raise ErrorArquitecturaInvalida(f"Python {bits} bits")

	# Verificar versión de Python
	if sys.version_info < (3, 13):
		version_actual = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
		raise ErrorVersionPython(version_actual)


def _asegurar_directorio_registrado() -> Path:
	"""
	Se asegura de que el directorio de DLL esté registrado para carga.

	Retorna:
		Path al directorio de DLL.
	"""
	global _directorio_registrado

	if not _directorio_registrado:
		directorio = registrar_directorio_dlls()
		if directorio is None:
			raise ErrorDLLNoEncontrada(
				"directorio x64",
				"No se pudo localizar el directorio de DLL",
			)
		_directorio_registrado = True
		return directorio

	return obtener_directorio_dlls()


def cargar_dll(nombre: str, obligatoria: bool = True) -> Optional[ctypes.WinDLL]:
	"""
	Carga una DLL desde el directorio de DLL del paquete.

	Utiliza caché para evitar cargar la misma DLL múltiples veces.
	Las DLL de BASS usan convención __stdcall (ctypes.WinDLL).

	Parámetros:
		nombre: Nombre de la DLL sin extensión (ej: 'bass', 'bassmix').
		obligatoria: Si True, lanza excepción si no se encuentra.
			Si False, retorna None.

	Retorna:
		ctypes.WinDLL con la DLL cargada, o None si es opcional y no existe.

	Lanza:
		ErrorDLLNoEncontrada: Si la DLL es obligatoria y no se encuentra.
	"""
	# Verificar caché
	clave = nombre.lower()
	if clave in _dlls_cargadas:
		return _dlls_cargadas[clave]

	# Asegurar directorio registrado
	directorio = _asegurar_directorio_registrado()

	# Construir ruta completa
	nombre_archivo = f"{nombre}.dll"
	ruta_dll = directorio / nombre_archivo

	if not ruta_dll.exists():
		if obligatoria:
			raise ErrorDLLNoEncontrada(nombre_archivo, str(directorio))
		return None

	try:
		dll = ctypes.WinDLL(str(ruta_dll))
		_dlls_cargadas[clave] = dll
		return dll
	except OSError as e:
		if obligatoria:
			raise ErrorDLLNoEncontrada(
				nombre_archivo,
				f"{directorio} (Error del sistema: {e})",
			)
		return None


def obtener_dll(nombre: str) -> Optional[ctypes.WinDLL]:
	"""
	Obtiene una DLL previamente cargada desde la caché.

	Parámetros:
		nombre: Nombre de la DLL sin extensión.

	Retorna:
		ctypes.WinDLL si está en caché, None si no.
	"""
	return _dlls_cargadas.get(nombre.lower())


def dlls_cargadas() -> dict[str, ctypes.WinDLL]:
	"""
	Devuelve un diccionario con todas las DLL cargadas.

	Retorna:
		dict con nombre -> ctypes.WinDLL.
	"""
	return dict(_dlls_cargadas)


def descargar_todas() -> None:
	"""
	Limpia la caché de DLL cargadas.

	Nota: Las DLL no se pueden descargar realmente de memoria
	en Python/ctypes, pero esto limpia las referencias.
	"""
	global _directorio_registrado
	_dlls_cargadas.clear()
	_directorio_registrado = False


# ─── Registro de DLL conocidas ───────────────────────────────────────

# DLL principal (obligatoria)
DLL_PRINCIPAL = "bass"

# Add-ons con sus nombres de DLL
ADDONS_CONOCIDOS: dict[str, str] = {
	"bass_fx": "bass_fx",
	"bassmix": "bassmix",
	"bassenc": "bassenc",
	"bassenc_flac": "bassenc_flac",
	"bassenc_mp3": "bassenc_mp3",
	"bassenc_ogg": "bassenc_ogg",
	"bassenc_opus": "bassenc_opus",
	"bassmidi": "bassmidi",
	"basswasapi": "basswasapi",
	"bassflac": "bassflac",
	"bassopus": "bassopus",
	"basswma": "basswma",
	"basshls": "basshls",
	"basscd": "basscd",
	"bassdsd": "bassdsd",
	"bassloud": "bassloud",
	"basswebm": "basswebm",
	"basswv": "basswv",
	"bassalac": "bassalac",
	"bassape": "bassape",
	"bass_aac": "bass_aac",
	"bass_ac3": "bass_ac3",
	"bass_mpc": "bass_mpc",
	"bass_spx": "bass_spx",
	"bass_tta": "bass_tta",
	"bass_ofr": "bass_ofr",
	"bass_vst": "bass_vst",
	"bass_ssl": "bass_ssl",
	"tags": "tags",
}


def cargar_bass() -> ctypes.WinDLL:
	"""
	Carga la DLL principal bass.dll.

	Es la primera DLL que debe cargarse. Todas las demás
	dependen de ella.

	Retorna:
		ctypes.WinDLL con bass.dll cargada.

	Lanza:
		ErrorDLLNoEncontrada: Si bass.dll no se encuentra.
		ErrorArquitecturaInvalida: Si no es Windows 64 bits.
		ErrorVersionPython: Si Python < 3.13.
	"""
	validar_plataforma()
	return cargar_dll(DLL_PRINCIPAL, obligatoria=True)


def cargar_addon(nombre: str) -> Optional[ctypes.WinDLL]:
	"""
	Carga un add-on de BASS por su nombre clave.

	El add-on se carga como opcional (no lanza excepción si no existe).
	Se requiere que bass.dll esté cargada previamente.

	Parámetros:
		nombre: Clave del add-on (ej: 'bassmix', 'bass_fx').

	Retorna:
		ctypes.WinDLL si se cargó correctamente, None si no existe.
	"""
	nombre_dll = ADDONS_CONOCIDOS.get(nombre, nombre)
	return cargar_dll(nombre_dll, obligatoria=False)


def cargar_todos_los_addons() -> dict[str, ctypes.WinDLL]:
	"""
	Intenta cargar todos los add-ons conocidos.

	Retorna:
		dict con nombre_addon -> ctypes.WinDLL para los que se cargaron.
	"""
	cargados = {}
	for clave in ADDONS_CONOCIDOS:
		dll = cargar_addon(clave)
		if dll is not None:
			cargados[clave] = dll
	return cargados
