"""
Client MQTT pour le service Motion
"""

from __future__ import annotations

import json
import threading
import time
from typing import Dict, Any, Callable, Optional, Tuple
from queue import Queue, Empty
from pathlib import Path

import paho.mqtt.client as mqtt

from .utils import setup_json_logger


def _rc_to_text(rc: int) -> str:
    mapping = {
        0: "SUCCESS",
        1: "ERR_NOMEM",
        2: "ERR_PROTOCOL",
        3: "ERR_INVAL",
        4: "ERR_NO_CONN",
        5: "CONN_REFUSED",
        6: "ERR_NOT_FOUND",
        7: "CONNECTION_LOST",
        8: "ERR_TLS",
        9: "ERR_PAYLOAD_SIZE",
        10: "ERR_NOT_SUPPORTED",
        11: "ERR_AUTH",
        12: "ERR_ACL_DENIED",
        13: "ERR_UNKNOWN",
        14: "ERR_ERRNO",
        15: "ERR_QUEUE_SIZE",
    }
    return mapping.get(rc, f"RC_{rc}")


class MotionMQTTClient:
    """Client MQTT pour le service Motion.

    - Auto-reconnexion avec backoff.
    - File de publications, vidée à la reconnexion.
    - API attendue par main.py : initialize(), publish_capabilities(),
      publish_health(), publish_estop_status(), cleanup().
    - Relais des messages orchestrateur vers on_orchestrator_state(payload_str).
    """

    # Topic qui porte le heartbeat de l'orchestrateur (nourrit le watchdog)
    ORCHESTRATOR_STATE_TOPIC = "orchestrator/state"

    def __init__(
        self,
        host: str,
        port: int = 1883,
        client_id: Optional[str] = "skull-motion",
        keepalive: int = 30,
        username: Optional[str] = None,
        password: Optional[str] = None,
        on_message: Optional[Callable[[str, str], None]] = None,
    ) -> None:
        self.host = host
        self.port = int(port)
        self.keepalive = int(keepalive)
        self.username = username
        self.password = password

        # Callback générique (optionnelle)
        self._on_message_cb = on_message
        # Callback spécifique attendue par main.py (assignée à l’extérieur)
        self.on_orchestrator_state: Optional[Callable[[str], None]] = None

        self.logger = setup_json_logger(Path("/opt/Skull/logs/motion.log"))
        self.logger.name = "motion.mqtt"

        # Client Paho (v3.1.1 pour compatibilité maximale)
        self._client = mqtt.Client(
            client_id=client_id or "",
            protocol=mqtt.MQTTv311,
            clean_session=True,
        )
        if self.username:
            self._client.username_pw_set(self.username, self.password)

        self._client.on_connect = self._on_connect
        self._client.on_disconnect = self._on_disconnect
        self._client.on_message = self._on_message

        # Reconnexion
        self._client.reconnect_delay_set(min_delay=1, max_delay=30)
        try:
            # Paho >= 1.6
            self._client.reconnect_on_failure = True  # type: ignore[attr-defined]
        except Exception:
            pass

        self._connected = False
        self._stop_evt = threading.Event()

        # Queue: (topic, payload_str, qos, retain)
        self._pub_queue: "Queue[Tuple[str, str, int, bool]]" = Queue()

        self._loop_thread = threading.Thread(
            target=self._loop_forever, name="mqtt-loop", daemon=True
        )

    # ------------------------------------------------------------------ #
    # API publique (compat main.py)
    # ------------------------------------------------------------------ #
    def initialize(self) -> bool:
        """Alias de start() pour uniformiser avec les autres composants."""
        try:
            self.start()
            return True
        except Exception as e:
            self.logger.error(f"Echec initialize() MQTT: {e}")
            return False

    def start(self) -> None:
        if not self._loop_thread.is_alive():
            self._loop_thread.start()

    def stop(self) -> None:
        """Arrêt propre et idempotent du client."""
        self._stop_evt.set()
        try:
            self._client.loop_stop()
            self._client.disconnect()
        except Exception:
            pass
        self._connected = False
        self.logger.info("Client MQTT fermé")

    def cleanup(self) -> None:
        """Compatibilité avec main.py : alias explicite de stop()."""
        self.stop()

    def is_connected(self) -> bool:
        return self._connected

    # Publications génériques
    def publish(
        self, topic: str, payload: Any, qos: int = 0, retain: bool = False
    ) -> bool:
        """Publication asynchrone. Met en file si déconnecté, pour ne rien perdre."""
        try:
            payload_str = (
                json.dumps(payload, separators=(",", ":"))
                if isinstance(payload, (dict, list))
                else str(payload)
            )
        except Exception:
            payload_str = str(payload)

        # Si connecté, essayer tout de suite
        if self._connected:
            try:
                res = self._client.publish(topic, payload_str, qos=qos, retain=retain)
                res.wait_for_publish(timeout=2)
                if res.rc == mqtt.MQTT_ERR_SUCCESS:
                    self.logger.debug(f"MQTT publié: {topic} = {payload_str}")
                    return True
                else:
                    self.logger.warning(
                        f"Publication immédiate échouée (rc={res.rc}), mise en file: {topic}"
                    )
            except Exception as e:
                self.logger.warning(
                    f"Publication immédiate exception ({e}), mise en file: {topic}"
                )

        # Sinon / en cas d’échec, mise en file
        self._pub_queue.put((topic, payload_str, qos, retain))
        return True

    # API spécifique attendue par main.py
    def publish_capabilities(self, capabilities: Dict[str, Dict[str, Any]]) -> bool:
        """Publie les capacités des servos (retained) sur {servo}/capabilities"""
        ok = True
        for servo_name, caps in capabilities.items():
            topic = f"{servo_name}/capabilities"
            if not self.publish(topic, caps, qos=0, retain=True):
                ok = False
        return ok

    def publish_health(self, status: Dict[str, Any]) -> bool:
        """Publie l’état de santé (non retained)"""
        return self.publish("motion/health", status, qos=0, retain=False)

    def publish_estop_status(self, active: bool) -> bool:
        """Publie l’état de l’E-Stop (retained)"""
        return self.publish(
            "motion/estop", {"active": bool(active)}, qos=1, retain=True
        )

    # ------------------------------------------------------------------ #
    # Boucle & callbacks
    # ------------------------------------------------------------------ #
    def _loop_forever(self) -> None:
        backoff = 1.0
        while not self._stop_evt.is_set():
            if not self._connected:
                try:
                    self._client.connect(self.host, self.port, keepalive=self.keepalive)
                    self._client.loop_start()
                    backoff = 1.0
                except Exception as e:
                    self.logger.warning(
                        f"Echec connexion MQTT vers {self.host}:{self.port} — retry dans {int(backoff)}s: {e}"
                    )
                    time.sleep(backoff)
                    backoff = min(backoff * 2.0, 30.0)
                    continue

            # Drainer la file des publications
            self._drain_pub_queue()
            time.sleep(0.2)

    def _drain_pub_queue(self) -> None:
        drained = 0
        while True:
            try:
                topic, payload_str, qos, retain = self._pub_queue.get_nowait()
            except Empty:
                break

            try:
                res = self._client.publish(topic, payload_str, qos=qos, retain=retain)
                res.wait_for_publish(timeout=2)
                if res.rc == mqtt.MQTT_ERR_SUCCESS:
                    drained += 1
                else:
                    # échec: on remet en file et on arrête de drainer (on retentera après)
                    self._pub_queue.put((topic, payload_str, qos, retain))
                    break
            except Exception as e:
                self.logger.warning(f"Publication différée échouée sur {topic}: {e}")
                # on remet en file et on retentera plus tard
                self._pub_queue.put((topic, payload_str, qos, retain))
                break

        if drained:
            self.logger.info(f"{drained} messages MQTT vidés après reconnexion")

    # Paho callbacks
    def _on_connect(self, client: mqtt.Client, userdata, flags, rc: int) -> None:
        if rc == 0:
            self._connected = True
            self.logger.info(f"Connecté au broker MQTT {self.host}:{self.port}")

            # S'abonner au heartbeat orchestrateur (nourrit le watchdog)
            try:
                client.subscribe(self.ORCHESTRATOR_STATE_TOPIC, qos=0)
                self.logger.info(f"Abonné à {self.ORCHESTRATOR_STATE_TOPIC}")
            except Exception as e:
                self.logger.error(
                    f"Echec subscribe {self.ORCHESTRATOR_STATE_TOPIC}: {e}"
                )

            # Vider la file
            self._drain_pub_queue()
        else:
            self._connected = False
            self.logger.warning(f"Echec de connexion MQTT: rc={rc} ({_rc_to_text(rc)})")

    def _on_disconnect(self, client: mqtt.Client, userdata, rc: int) -> None:
        self._connected = False
        if rc == 0:
            self.logger.info("Déconnexion MQTT demandée (rc=0)")
        else:
            self.logger.warning(
                f"Déconnexion MQTT inattendue: code {rc} ({_rc_to_text(rc)})"
            )

    def _on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage) -> None:
        topic = msg.topic or ""
        payload = msg.payload.decode("utf-8", errors="replace")

        # Callback spécifique pour le heartbeat orchestrateur
        if topic == self.ORCHESTRATOR_STATE_TOPIC:
            if self.on_orchestrator_state:
                try:
                    self.on_orchestrator_state(payload)
                except Exception as e:
                    self.logger.error(f"Callback on_orchestrator_state a échoué: {e}")
            else:
                # Même sans callback, on loggue la réception
                self.logger.debug(f"Heartbeat orchestrateur reçu: {payload}")
            return

        # Callback générique éventuelle
        if self._on_message_cb:
            try:
                self._on_message_cb(topic, payload)
                return
            except Exception as e:
                self.logger.error(f"Callback on_message a échoué: {e}")

        # À défaut, log en debug
        self.logger.debug(f"Message reçu sur {topic}: {payload}")
