"""
Gestion de la configuration du service Motion
"""

import json
import os
from pathlib import Path
from typing import Dict, Any, Optional
from dataclasses import dataclass, field

from .exceptions import ConfigurationError
from .utils import setup_json_logger


@dataclass
class ServoConfig:
    """Configuration d'un servo"""

    min_deg: float
    max_deg: float
    center_deg: float
    min_us: int
    max_us: int
    curve: str = "linear"
    channel: Optional[int] = None

    def __post_init__(self):
        if self.min_deg >= self.max_deg:
            raise ConfigurationError(
                f"min_deg ({self.min_deg}) >= max_deg ({self.max_deg})"
            )
        if not (self.min_deg <= self.center_deg <= self.max_deg):
            raise ConfigurationError(
                f"center_deg ({self.center_deg}) hors limites [{self.min_deg}, {self.max_deg}]"
            )
        if self.min_us >= self.max_us:
            raise ConfigurationError(
                f"min_us ({self.min_us}) >= max_us ({self.max_us})"
            )


@dataclass
class MotionConfig:
    """Configuration complète du service Motion"""

    servos: Dict[str, ServoConfig] = field(default_factory=dict)
    mqtt_host: str = "127.0.0.1"
    mqtt_port: int = 1883
    pca9685_addr: int = 0x40
    pca9685_freq: int = 50
    estop_gpio: int = 23
    loop_freq_hz: int = 50
    watchdog_timeout_ms: int = 5000

    # Noms canoniques attendus par le service
    REQUIRED_SERVOS = {"jaw", "eye_left_h", "eye_right_h", "neck_pan"}

    # Alias acceptés pour compatibilité
    NAME_ALIASES = {
        "eye_l_h": "eye_left_h",
        "eye_r_h": "eye_right_h",
        # identités (pour clarté)
        "jaw": "jaw",
        "eye_left_h": "eye_left_h",
        "eye_right_h": "eye_right_h",
        "neck_pan": "neck_pan",
    }

    # Canaux par défaut si absents du JSON
    DEFAULT_CHANNELS = {"jaw": 0, "eye_left_h": 1, "eye_right_h": 2, "neck_pan": 3}

    @classmethod
    def from_files(
        cls, servos_file: Path, env_file: Optional[Path] = None
    ) -> "MotionConfig":
        """Charge la configuration depuis les fichiers"""
        logger = setup_json_logger(Path("/opt/Skull/logs/motion.log"))
        logger.name = "motion.config"

        # Charger servos.json
        try:
            with open(servos_file, "r", encoding="utf-8") as f:
                raw = f.read()
            root = json.loads(raw)
            logger.info(f"Configuration servos chargée depuis {servos_file}")
        except Exception as e:
            raise ConfigurationError(f"Erreur lecture {servos_file}: {e}")

        # Détecter la présence d'un wrapper { "servos": { ... }, ... }
        if isinstance(root, dict) and "servos" in root:
            data_servos = root.get("servos")
            if not isinstance(data_servos, dict):
                raise ConfigurationError(
                    f"Clé 'servos' trouvée dans {servos_file}, mais de type {type(data_servos).__name__} (attendu: objet JSON)"
                )
            logger.info("Format servos.json détecté: servos sous la clé 'servos'")
        elif isinstance(root, dict):
            data_servos = root
            logger.info("Format servos.json détecté: servos à la racine")
        else:
            raise ConfigurationError(
                f"Format inattendu pour {servos_file}: type {type(root).__name__} (attendu: objet JSON)"
            )

        # Charger .env si fourni
        if env_file and env_file.exists():
            try:
                from dotenv import load_dotenv

                load_dotenv(env_file)
                logger.info(f"Variables d'environnement chargées depuis {env_file}")
            except ImportError:
                logger.warning("python-dotenv non disponible, .env ignoré")

        # Créer config
        config = cls()

        # Variables d'environnement (prioritaires)
        config.mqtt_host = os.getenv("MQTT_HOST", config.mqtt_host)
        config.mqtt_port = int(os.getenv("MQTT_PORT", str(config.mqtt_port)))
        config.pca9685_addr = int(
            os.getenv("PCA9685_ADDR", str(config.pca9685_addr)), 0
        )
        config.pca9685_freq = int(os.getenv("PCA9685_FREQ", str(config.pca9685_freq)))
        # estop_gpio: si non défini en env, on regardera _safety_limits plus bas
        env_estop = os.getenv("ESTOP_GPIO")
        if env_estop is not None:
            config.estop_gpio = int(env_estop)

        # Lire éventuellement des limites de sécurité depuis le JSON (si env absent)
        safety = root.get("_safety_limits") if isinstance(root, dict) else None
        if env_estop is None and isinstance(safety, dict) and "estop_gpio" in safety:
            try:
                config.estop_gpio = int(safety["estop_gpio"])
                logger.info(
                    f"ESTOP_GPIO défini depuis servos.json: {config.estop_gpio}"
                )
            except Exception:
                logger.warning(
                    "Valeur 'estop_gpio' invalide dans _safety_limits, ignorée"
                )

        if isinstance(safety, dict) and "watchdog_timeout_s" in safety:
            try:
                timeout_s = float(safety["watchdog_timeout_s"])
                config.watchdog_timeout_ms = int(round(timeout_s * 1000.0))
                logger.info(
                    f"Watchdog timeout défini depuis servos.json: {config.watchdog_timeout_ms} ms"
                )
            except Exception:
                logger.warning(
                    "Valeur 'watchdog_timeout_s' invalide dans _safety_limits, ignorée"
                )

        # Servos
        seen_canonical: set[str] = set()
        for name, data in data_servos.items():
            # Ignorer les méta-entrées au niveau des noms
            if name.startswith("_"):
                logger.debug(f"Clé méta '{name}' ignorée")
                continue

            canonical = cls.NAME_ALIASES.get(name, name)

            if canonical not in cls.REQUIRED_SERVOS:
                logger.warning(f"Servo '{name}' ignoré (nom/canal inconnu)")
                continue

            if not isinstance(data, dict):
                raise ConfigurationError(
                    f"Definition servo '{name}' invalide: type {type(data).__name__}"
                )

            try:
                min_deg = float(data["min_deg"])
                max_deg = float(data["max_deg"])
                center_deg = float(data["center_deg"])
                min_us = int(data["min_us"])
                max_us = int(data["max_us"])
                curve = str(data.get("curve", "linear"))
                # on prend le channel du JSON si présent, sinon défaut
                channel = data.get("channel", cls.DEFAULT_CHANNELS.get(canonical))
                if channel is None:
                    raise ConfigurationError(
                        f"Servo '{name}': aucun canal spécifié et pas de valeur par défaut"
                    )
                channel = int(channel)

                servo_config = ServoConfig(
                    min_deg=min_deg,
                    max_deg=max_deg,
                    center_deg=center_deg,
                    min_us=min_us,
                    max_us=max_us,
                    curve=curve,
                    channel=channel,
                )
                # si on rencontre à la fois l'alias et le canonique, priorité au canonique
                if canonical in config.servos and name != canonical:
                    logger.info(
                        f"Alias '{name}' ignoré: '{canonical}' déjà fourni. (priorité au nom canonique)"
                    )
                    continue

                config.servos[canonical] = servo_config
                seen_canonical.add(canonical)
                if name != canonical:
                    logger.info(f"Servo '{name}' mappé vers '{canonical}' (alias)")

                logger.info(
                    f"Servo '{canonical}' configuré sur canal {servo_config.channel}"
                )
            except KeyError as ke:
                raise ConfigurationError(
                    f"Champ manquant pour le servo '{name}': {ke}"
                ) from ke
            except Exception as e:
                raise ConfigurationError(f"Erreur configuration servo '{name}': {e}")

        # Vérifier servos requis
        missing = cls.REQUIRED_SERVOS - set(config.servos.keys())
        if missing:
            available = ", ".join(sorted(data_servos.keys()))
            raise ConfigurationError(
                f"Servos manquants: {missing}. Clés disponibles dans le fichier: {available or '(aucune)'}"
            )

        logger.info(
            f"Configuration complète: {len(config.servos)} servos, PCA@{hex(config.pca9685_addr)}"
        )
        return config
