"""Client RabbitMQ pour publication et consommation de messages."""

from __future__ import annotations

import json
from typing import Callable

import pika
from pika.exceptions import AMQPConnectionError, AMQPChannelError
from pydantic import BaseModel
from opentelemetry.propagate import inject, extract
from opentelemetry import context as otel_context

from .exceptions import RabbitMQError, RetryableError, NonRetryableError


class RabbitMQPublisher:
    """Publisher RabbitMQ avec propagation OTel."""

    def __init__(
        self,
        host: str,
        port: int,
        user: str,
        password: str,
        vhost: str = "/",
    ) -> None:
        """Initialise le publisher RabbitMQ.

        Args:
            host: Hôte RabbitMQ.
            port: Port RabbitMQ.
            user: Nom d'utilisateur.
            password: Mot de passe.
            vhost: Virtual host.

        Raises:
            RabbitMQError: Si la connexion échoue.
        """
        self._host = host
        self._port = port
        self._credentials = pika.PlainCredentials(user, password)
        self._vhost = vhost
        self._connection: pika.BlockingConnection | None = None
        self._channel: pika.channel.Channel | None = None
        self._connect()

    def _connect(self) -> None:
        """Établit la connexion à RabbitMQ."""
        try:
            parameters = pika.ConnectionParameters(
                host=self._host,
                port=self._port,
                virtual_host=self._vhost,
                credentials=self._credentials,
                heartbeat=0,
            )
            self._connection = pika.BlockingConnection(parameters)
            self._channel = self._connection.channel()
        except AMQPConnectionError as e:
            raise RabbitMQError(f"Échec de connexion à RabbitMQ: {e}") from e
        except Exception as e:
            raise RabbitMQError(f"Erreur lors de la connexion RabbitMQ: {e}") from e

    def _ensure_connected(self) -> None:
        """S'assure que la connexion est active, reconnecte si nécessaire."""
        if self._connection is None or self._connection.is_closed:
            self._connect()
        elif self._channel is None or self._channel.is_closed:
            try:
                self._channel = self._connection.channel()
            except Exception:
                self._connect()

    def publish(
        self,
        exchange: str,
        routing_key: str,
        message: dict | BaseModel,
    ) -> None:
        """Publie un message avec contexte OTel.

        Args:
            exchange: Nom de l'exchange.
            routing_key: Clé de routage.
            message: Message à publier (dict ou Pydantic model).

        Raises:
            RabbitMQError: Si la publication échoue.
        """
        try:
            self._ensure_connected()

            if isinstance(message, BaseModel):
                body = message.model_dump_json().encode("utf-8")
            else:
                body = json.dumps(message, ensure_ascii=False).encode("utf-8")

            headers: dict[str, str] = {}
            inject(headers)

            properties = pika.BasicProperties(
                content_type="application/json",
                delivery_mode=2,  # Persistent
                headers=headers if headers else None,
            )

            self._channel.basic_publish(
                exchange=exchange,
                routing_key=routing_key,
                body=body,
                properties=properties,
            )
        except AMQPChannelError as e:
            raise RabbitMQError(
                f"Échec de publication sur RabbitMQ ({exchange}/{routing_key}): {e}"
            ) from e
        except AMQPConnectionError as e:
            raise RabbitMQError(
                f"Connexion RabbitMQ perdue lors de la publication: {e}"
            ) from e
        except Exception as e:
            raise RabbitMQError(
                f"Erreur lors de la publication ({exchange}/{routing_key}): {e}"
            ) from e

    def close(self) -> None:
        """Ferme la connexion."""
        try:
            if self._connection and not self._connection.is_closed:
                self._connection.close()
        except Exception:
            pass
        finally:
            self._connection = None
            self._channel = None

    def __enter__(self) -> RabbitMQPublisher:
        """Support du context manager."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Ferme la connexion à la sortie du context manager."""
        self.close()


class RabbitMQConsumer:
    """Consumer RabbitMQ avec gestion automatique des ack/nack."""

    def __init__(
        self,
        host: str,
        port: int,
        user: str,
        password: str,
        vhost: str = "/",
    ) -> None:
        """Initialise le consumer RabbitMQ.

        Args:
            host: Hôte RabbitMQ.
            port: Port RabbitMQ.
            user: Nom d'utilisateur.
            password: Mot de passe.
            vhost: Virtual host.

        Raises:
            RabbitMQError: Si la connexion échoue.
        """
        self._host = host
        self._port = port
        self._credentials = pika.PlainCredentials(user, password)
        self._vhost = vhost
        self._connection: pika.BlockingConnection | None = None
        self._channel: pika.channel.Channel | None = None
        self._should_stop = False
        self._consumer_tag: str | None = None
        self._connect()

    def _connect(self) -> None:
        """Établit la connexion à RabbitMQ."""
        try:
            parameters = pika.ConnectionParameters(
                host=self._host,
                port=self._port,
                virtual_host=self._vhost,
                credentials=self._credentials,
                heartbeat=0,
            )
            self._connection = pika.BlockingConnection(parameters)
            self._channel = self._connection.channel()
        except AMQPConnectionError as e:
            raise RabbitMQError(f"Échec de connexion à RabbitMQ: {e}") from e
        except Exception as e:
            raise RabbitMQError(f"Erreur lors de la connexion RabbitMQ: {e}") from e

    def consume(
        self,
        queue: str,
        callback: Callable[[dict], None],
        prefetch_count: int = 1,
    ) -> None:
        """Consomme les messages d'une queue.

        Le callback reçoit le message désérialisé.
        - Si callback réussit: ack automatique
        - Si callback lève RetryableError: nack avec requeue
        - Si callback lève NonRetryableError: nack sans requeue
        - Si callback lève autre exception: nack sans requeue

        Args:
            queue: Nom de la queue.
            callback: Fonction de traitement recevant le message désérialisé.
            prefetch_count: Nombre de messages en parallèle.

        Raises:
            RabbitMQError: Si la consommation échoue.
        """
        try:
            self._channel.basic_qos(prefetch_count=prefetch_count)

            def on_message(
                channel: pika.channel.Channel,
                method: pika.spec.Basic.Deliver,
                properties: pika.BasicProperties,
                body: bytes,
            ) -> None:
                """Callback interne pour traiter les messages."""
                if self._should_stop:
                    channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
                    return

                ctx_token = None
                try:
                    headers = properties.headers or {}
                    ctx = extract(headers)
                    ctx_token = otel_context.attach(ctx)

                    message = json.loads(body.decode("utf-8"))
                    callback(message)

                    channel.basic_ack(delivery_tag=method.delivery_tag)

                except RetryableError:
                    channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)

                except NonRetryableError:
                    channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

                except Exception:
                    channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

                finally:
                    if ctx_token is not None:
                        otel_context.detach(ctx_token)

            self._consumer_tag = self._channel.basic_consume(
                queue=queue,
                on_message_callback=on_message,
                auto_ack=False,
            )

            while not self._should_stop:
                self._connection.process_data_events(time_limit=1)

        except AMQPChannelError as e:
            raise RabbitMQError(f"Erreur de canal lors de la consommation: {e}") from e
        except AMQPConnectionError as e:
            raise RabbitMQError(f"Connexion RabbitMQ perdue: {e}") from e
        except Exception as e:
            if self._should_stop:
                return
            raise RabbitMQError(f"Erreur lors de la consommation: {e}") from e

    def stop(self) -> None:
        """Arrête la consommation."""
        self._should_stop = True
        try:
            if self._consumer_tag and self._channel and not self._channel.is_closed:
                self._channel.basic_cancel(self._consumer_tag)
        except Exception:
            pass
        try:
            if self._connection and not self._connection.is_closed:
                self._connection.close()
        except Exception:
            pass
        finally:
            self._connection = None
            self._channel = None
            self._consumer_tag = None

    def __enter__(self) -> RabbitMQConsumer:
        """Support du context manager."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Arrête la consommation à la sortie du context manager."""
        self.stop()
