"""
BIAPAY SDK — Callback Server

Ce module expose un serveur HTTP léger intégré au SDK.
Il écoute sur l'URL de callback BiaPay, vérifie la signature HMAC-SHA256,
puis transmet le payload validé au handler défini par le développeur.

Usage typique::

    from biapay import BIAPay
    from biapay.models import CallbackPayload

    client = BIAPay(client_id="...", client_secret="...")

    @client.on_callback
    def handle_payment(payload: CallbackPayload):
        if payload.is_completed:
            print(f"Paiement reçu : {payload.amount} {payload.currency}")
            # Mettre à jour la commande en BDD, envoyer un email, etc.

    # Lance le serveur d'écoute sur le port 8080
    client.start_callback_server(host="0.0.0.0", port=8080, path="/payment/callback")

Intégration Django/Flask (sans serveur intégré)::

    # Dans votre view Django :
    payload = client.handle_callback_request(request.body, request.content_type)
"""

import json
import logging
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Callable, Optional
from urllib.parse import urlparse, parse_qs

from .models import CallbackPayload
from .exceptions import BIAPaySignatureError, BIAPayValidationError

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Réponses HTTP helpers
# ---------------------------------------------------------------------------

def _json_response(handler: "BaseHTTPRequestHandler", status: int, body: dict) -> None:
    """Envoie une réponse JSON."""
    data = json.dumps(body).encode("utf-8")
    handler.send_response(status)
    handler.send_header("Content-Type", "application/json")
    handler.send_header("Content-Length", str(len(data)))
    handler.end_headers()
    handler.wfile.write(data)


# ---------------------------------------------------------------------------
# Handler HTTP principal
# ---------------------------------------------------------------------------

class _CallbackHandler(BaseHTTPRequestHandler):
    """
    Handler HTTP interne du SDK BiaPay.

    BiaPay envoie les données soit en POST JSON (body),
    soit en GET avec query params selon la configuration du dashboard.
    Les deux modes sont supportés.
    """

    # Injectés par CallbackServer avant de démarrer
    callback_path: str = "/payment/callback"
    verify_fn: Callable = None        # BIAPay.verify_callback_signature
    user_handler: Callable = None     # handler du développeur

    def log_message(self, format, *args):
        """Redirige les logs HTTP vers le logger Python standard."""
        logger.debug("BiaPay CallbackServer: %s", format % args)

    # ------------------------------------------------------------------ #
    # POST — BiaPay envoie le payload en JSON dans le body                #
    # ------------------------------------------------------------------ #

    def do_POST(self):
        parsed = urlparse(self.path)
        if parsed.path.rstrip("/") != self.callback_path.rstrip("/"):
            _json_response(self, 404, {"error": "Not found"})
            return

        # Lecture du body
        content_length = int(self.headers.get("Content-Length", 0))
        raw_body = self.rfile.read(content_length) if content_length else b""

        try:
            params = json.loads(raw_body.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            logger.warning("BiaPay callback: body JSON invalide — %s", e)
            _json_response(self, 400, {"success": False, "error": "JSON invalide"})
            return

        self._process(params)

    # ------------------------------------------------------------------ #
    # GET — BiaPay envoie le payload en query params dans l'URL           #
    # ------------------------------------------------------------------ #

    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path.rstrip("/") != self.callback_path.rstrip("/"):
            _json_response(self, 404, {"error": "Not found"})
            return

        # Convertit les query params en dict simple (première valeur uniquement)
        raw_params = parse_qs(parsed.query)
        params = {k: v[0] for k, v in raw_params.items()}
        self._process(params)

    # ------------------------------------------------------------------ #
    # Logique commune : vérification + dispatch                           #
    # ------------------------------------------------------------------ #

    def _process(self, params: dict) -> None:
        """
        1. Parse les paramètres en CallbackPayload
        2. Vérifie la signature HMAC-SHA256
        3. Si valide → appelle le handler du développeur
        4. Si invalide → répond 403 avec un message d'erreur
        """
        ip = self.client_address[0]
        logger.info("BiaPay callback reçu depuis %s | params=%s", ip, params)

        # --- Parse ---
        try:
            payload = CallbackPayload.from_query_params(params)
        except Exception as e:
            logger.error("Impossible de parser le payload BiaPay: %s", e)
            _json_response(self, 400, {
                "success": False,
                "error": f"Payload invalide : {e}"
            })
            return

        # --- Vérification de la signature ---
        try:
            is_valid = self.verify_fn(payload)
        except BIAPaySignatureError as e:
            logger.warning("Signature absente dans le callback BiaPay: %s", e)
            _json_response(self, 400, {
                "success": False,
                "error": str(e)
            })
            return
        except Exception as e:
            logger.error("Erreur lors de la vérification de signature: %s", e)
            _json_response(self, 500, {
                "success": False,
                "error": "Erreur interne lors de la vérification"
            })
            return

        if not is_valid:
            logger.warning(
                "Signature BiaPay INVALIDE pour orderId=%s | "
                "La requête ne provient peut-être pas de BiaPay.",
                payload.order_id,
            )
            _json_response(self, 403, {
                "success": False,
                "error": (
                    "Signature invalide. "
                    "La requête ne provient pas d'une source BiaPay reconnue."
                ),
                "orderId": payload.order_id,
            })
            return

        # --- Signature valide → dispatch vers le handler du développeur ---
        logger.info(
            "Signature BiaPay valide ✓ | orderId=%s | status=%s | amount=%s %s",
            payload.order_id, payload.status, payload.amount, payload.currency,
        )

        try:
            self.user_handler(payload)
        except Exception as e:
            # On n'expose pas l'erreur interne à BiaPay mais on la logue
            logger.exception(
                "Erreur dans le handler utilisateur pour orderId=%s: %s",
                payload.order_id, e,
            )
            _json_response(self, 500, {
                "success": False,
                "error": "Erreur dans le traitement du paiement"
            })
            return

        _json_response(self, 200, {
            "success": True,
            "message": "Callback traité avec succès.",
            "orderId": payload.order_id,
            "status": payload.status,
        })


# ---------------------------------------------------------------------------
# Serveur d'écoute
# ---------------------------------------------------------------------------

class CallbackServer:
    """
    Serveur HTTP intégré au SDK BiaPay.

    Lance un serveur léger (optionnellement dans un thread daemon) qui
    écoute les notifications de paiement entrantes de BiaPay.

    Args:
        host: Adresse d'écoute (ex: "0.0.0.0" pour toutes les interfaces).
        port: Port d'écoute (ex: 8080).
        path: Chemin de l'URL de callback (ex: "/payment/callback").
        verify_fn: Fonction de vérification de signature (fournie par BIAPay).
        user_handler: Callable du développeur qui reçoit le CallbackPayload valide.
    """

    def __init__(
        self,
        host: str,
        port: int,
        path: str,
        verify_fn: Callable,
        user_handler: Callable,
    ):
        self.host = host
        self.port = port
        self.path = path
        self._server: Optional[HTTPServer] = None
        self._thread: Optional[threading.Thread] = None

        # Injecte les fonctions dans la classe handler
        # (pas d'instance par requête → attributs de classe)
        _CallbackHandler.callback_path = path
        _CallbackHandler.verify_fn = staticmethod(verify_fn)
        _CallbackHandler.user_handler = staticmethod(user_handler)

    def start(self, blocking: bool = True) -> None:
        """
        Démarre le serveur de callback.

        Args:
            blocking: Si True (défaut), bloque le thread courant (usage script/standalone).
                      Si False, démarre dans un thread daemon en arrière-plan
                      (usage dans une app Django/Flask existante).
        """
        self._server = HTTPServer((self.host, self.port), _CallbackHandler)
        url = f"http://{self.host}:{self.port}{self.path}"
        logger.info("BiaPay CallbackServer démarré sur %s", url)
        print(f"✅ BiaPay CallbackServer écoute sur : {url}")

        if blocking:
            try:
                self._server.serve_forever()
            except KeyboardInterrupt:
                self.stop()
        else:
            self._thread = threading.Thread(
                target=self._server.serve_forever,
                daemon=True,
                name="biapay-callback-server",
            )
            self._thread.start()

    def stop(self) -> None:
        """Arrête proprement le serveur."""
        if self._server:
            self._server.shutdown()
            self._server.server_close()
            logger.info("BiaPay CallbackServer arrêté.")
            print("🛑 BiaPay CallbackServer arrêté.")

    @property
    def is_running(self) -> bool:
        return self._thread is not None and self._thread.is_alive()
