"""
BIAPAY SDK Client
"""

import hashlib
import hmac
import base64
import json
import logging
from typing import Optional, Callable

import requests
from requests.exceptions import ConnectionError, Timeout, RequestException

from .models import (
    PaymentSession,
    OrderDetails,
    CustomerDetails,
    ClientDetails,
    CallbackPayload,
)
from .exceptions import (
    BIAPayError,
    BIAPayAuthError,
    BIAPayConnectionError,
    BIAPaySignatureError,
    BIAPayValidationError,
)
from .callback_server import CallbackServer

logger = logging.getLogger(__name__)

DEFAULT_BASE_URL = "https://api.biapay.net/gateway"
DEFAULT_TIMEOUT = 30  # seconds


class BIAPay:
    """BIAPAY Payment Gateway Client.

    This is the main entry point for interacting with the BIAPAY payment gateway.
    It handles session creation, iframe URL generation, and callback verification.

    Args:
        client_id: Your BIAPAY POS client ID.
        client_secret: Your BIAPAY POS client secret.
        base_url: API base URL (defaults to production). Override for sandbox/dev.
        timeout: HTTP request timeout in seconds (default: 30).
        originated_by: Channel identifier (default: "2" for web).

    Example::

        from biapay import BIAPay

        client = BIAPay(
            client_id="6ec5129d-4a8a-49d7-a10f-eb31caa1232b",
            client_secret="V059WR536h84h5357h3jzgh8nhw6h55g"
        )

        # Create a payment session
        session = client.create_payment_session(
            order_id="ORDER-123",
            amount=5000,
            currency_code="XAF",
            customer_email="john@example.com",
            customer_phone="237600000000",
            transaction_id="TXN-123"
        )

        # Embed in your page
        print(session.get_iframe_html())
    """

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: int = DEFAULT_TIMEOUT,
        originated_by: str = "2",
    ):
        if not client_id or not client_secret:
            raise BIAPayValidationError("client_id and client_secret are required.")

        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.originated_by = originated_by

        self._session = requests.Session()
        self._session.headers.update({
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": "biapay-python-sdk/1.0.0",
        })

        # Callback handler enregistré via @client.on_callback
        self._callback_handler: Optional[Callable[[CallbackPayload], None]] = None
        self._callback_server: Optional[CallbackServer] = None

    # -------------------------------------------------------------------------
    # Public API
    # -------------------------------------------------------------------------

    def on_callback(self, fn: Callable[[CallbackPayload], None]) -> Callable:
        """Décorateur pour enregistrer le handler de callback BiaPay.

        La fonction décorée est appelée uniquement si la signature HMAC est valide.
        Si la signature est invalide, le SDK répond automatiquement 403 et
        n'appelle PAS ce handler.

        Args:
            fn: Callable qui reçoit un :class:`CallbackPayload` vérifié.

        Returns:
            La fonction originale (pour pouvoir l'utiliser en dehors du décorateur).

        Example::

            @client.on_callback
            def handle_payment(payload: CallbackPayload):
                if payload.is_completed:
                    print(f"✅ Paiement reçu : {payload.amount} {payload.currency}")
                    # update_order_in_db(payload.order_id)
                else:
                    print(f"❌ Paiement échoué pour {payload.order_id}")
        """
        self._callback_handler = fn
        logger.debug("Handler de callback enregistré : %s", fn.__name__)
        return fn

    def start_callback_server(
        self,
        host: str = "0.0.0.0",
        port: int = 8080,
        path: str = "/payment/callback",
        blocking: bool = True,
    ) -> None:
        """Lance le serveur HTTP intégré qui écoute les callbacks BiaPay.

        Ce serveur :
          1. Reçoit le POST (JSON body) ou GET (query params) envoyé par BiaPay
          2. Parse le payload en :class:`CallbackPayload`
          3. Vérifie la signature HMAC-SHA256 avec votre ``client_secret``
          4. Si valide → appelle le handler enregistré via ``@client.on_callback``
          5. Si invalide → répond 403 sans appeler votre handler

        Args:
            host: Interface d'écoute. "0.0.0.0" pour toutes les interfaces.
            port: Port d'écoute (défaut: 8080).
            path: Chemin de l'URL callback (défaut: "/payment/callback").
                  Doit correspondre à l'URL configurée dans le dashboard BiaPay.
            blocking: Si True (défaut), bloque le thread courant.
                      Si False, démarre en arrière-plan (thread daemon).

        Raises:
            RuntimeError: Si aucun handler n'a été enregistré via ``@client.on_callback``.

        Example (script standalone)::

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

            @client.on_callback
            def handle(payload):
                print(payload.status)

            client.start_callback_server(port=8080)
            # Bloqueant — configurer dans BiaPay dashboard : http://your-server:8080/payment/callback

        Example (thread en arrière-plan)::

            client.start_callback_server(port=8080, blocking=False)
            # Le reste du code s'exécute normalement
        """
        if self._callback_handler is None:
            raise RuntimeError(
                "Aucun handler de callback enregistré. "
                "Utilisez @client.on_callback avant d'appeler start_callback_server()."
            )

        self._callback_server = CallbackServer(
            host=host,
            port=port,
            path=path,
            verify_fn=self.verify_callback_signature,
            user_handler=self._callback_handler,
        )
        self._callback_server.start(blocking=blocking)

    def stop_callback_server(self) -> None:
        """Arrête proprement le serveur de callback si il est actif."""
        if self._callback_server:
            self._callback_server.stop()

    def handle_callback_request(
        self,
        body: bytes,
        content_type: str = "application/json",
    ) -> CallbackPayload:
        """Traite un callback BiaPay depuis votre propre framework (Django, Flask, FastAPI…).

        Utilisez cette méthode si vous avez déjà un serveur web et que vous voulez
        simplement déléguer la vérification au SDK sans lancer le serveur intégré.

        Le SDK vérifie la signature et retourne le payload si valide,
        ou lève une exception si la signature est invalide.

        Args:
            body: Corps brut de la requête HTTP (``request.body`` en Django,
                  ``await request.body()`` en FastAPI, etc.).
            content_type: Content-Type de la requête (défaut: "application/json").

        Returns:
            Un :class:`CallbackPayload` vérifié et prêt à utiliser.

        Raises:
            BIAPaySignatureError: Si la signature est absente ou invalide.
            BIAPayValidationError: Si le payload ne peut pas être parsé.

        Example (Django view)::

            def payment_callback(request):
                try:
                    payload = client.handle_callback_request(request.body)
                except BIAPaySignatureError as e:
                    return JsonResponse({"error": str(e)}, status=403)

                if payload.is_completed:
                    Order.objects.filter(id=payload.order_id).update(status="paid")
                return JsonResponse({"success": True})

        Example (FastAPI)::

            @app.post("/payment/callback")
            async def payment_callback(request: Request):
                body = await request.body()
                payload = client.handle_callback_request(body)
                return {"success": True, "status": payload.status}
        """
        # Parse le body JSON
        try:
            if isinstance(body, (bytes, bytearray)):
                params = json.loads(body.decode("utf-8"))
            elif isinstance(body, str):
                params = json.loads(body)
            elif isinstance(body, dict):
                params = body
            else:
                raise BIAPayValidationError(f"Type de body non supporté : {type(body)}")
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            raise BIAPayValidationError(f"Payload JSON invalide : {e}") from e

        # Parse en CallbackPayload
        callback = CallbackPayload.from_query_params(params)

        # Vérification de la signature
        if not callback.biapay_signature:
            raise BIAPaySignatureError(
                "Signature absente dans le payload BiaPay."
            )

        is_valid = self.verify_callback_signature(callback)
        if not is_valid:
            logger.warning(
                "Signature BiaPay invalide pour orderId=%s", callback.order_id
            )
            raise BIAPaySignatureError(
                f"Signature invalide pour la commande '{callback.order_id}'. "
                "La requête ne provient pas d'une source BiaPay reconnue."
            )

        logger.info(
            "handle_callback_request: signature valide ✓ | orderId=%s | status=%s",
            callback.order_id, callback.status,
        )
        return callback

    def create_payment_session(
        self,
        order_id: str,
        amount: int,
        currency_code: str,
        customer_email: str,
        customer_phone: str,
        transaction_id: Optional[str] = None,
    ) -> PaymentSession:
        """Create a new payment session and return a PaymentSession object.

        This is the primary method to call when a customer initiates checkout.
        It authenticates with BIAPAY and returns session data including the
        `launch_url` to use as your iframe `src`.

        Args:
            order_id: Your unique order identifier.
            amount: Payment amount as an integer (e.g. 5000 for 5000 XAF).
            currency_code: ISO 4217 currency code (e.g. "XAF").
            customer_email: Customer's email address.
            customer_phone: Customer's mobile number with country code.
            transaction_id: Your transaction identifier. Defaults to order_id.

        Returns:
            A :class:`PaymentSession` containing the access token and iframe URLs.

        Raises:
            BIAPayValidationError: If required parameters are missing or invalid.
            BIAPayAuthError: If BIAPAY rejects the credentials.
            BIAPayConnectionError: If the network request fails.
            BIAPayError: For other API errors.

        Example::

            session = client.create_payment_session(
                order_id="ORDER-456",
                amount=10000,
                currency_code="XAF",
                customer_email="alice@example.com",
                customer_phone="237612345678",
                transaction_id="TXN-456"
            )
            iframe_html = session.get_iframe_html(lang="fr")
        """
        self._validate_payment_params(order_id, amount, currency_code, customer_email, customer_phone)

        if transaction_id is None:
            transaction_id = order_id

        payload = self._build_auth_payload(
            order_id=order_id,
            amount=amount,
            currency_code=currency_code,
            customer_email=customer_email,
            customer_phone=customer_phone,
            transaction_id=transaction_id,
        )

        response_data = self._post("/api/auth/login", payload)
        return PaymentSession.from_response(response_data)

    def verify_callback_signature(self, callback: CallbackPayload) -> bool:
        """Verify the HMAC signature on a BIAPAY callback payload.

        BIAPAY signs each callback with an HMAC-SHA256 signature using your
        client secret. You should always verify this before processing payments.

        The signature is computed as:
            HMAC-SHA256(orderId + '|' + biapayTransactionId + '|' + transactionId, client_secret)

        Args:
            callback: A :class:`CallbackPayload` instance (from `CallbackPayload.from_query_params()`).

        Returns:
            True if the signature is valid, False otherwise.

        Raises:
            BIAPaySignatureError: If the callback has no signature to verify.

        Example::

            payload = CallbackPayload.from_query_params(request.GET)

            if not client.verify_callback_signature(payload):
                return HttpResponse(status=403)

            if payload.is_completed:
                # fulfill the order
                pass
        """
        if not callback.biapay_signature:
            raise BIAPaySignatureError(
                "No biapaySignature found in callback. Cannot verify."
            )

        raw = (
            f"{callback.order_id}"
            f"|{callback.biapay_transaction_id}"
            f"|{callback.transaction_id}"
        )
        expected = self._compute_hmac(raw)

        # Case-insensitive comparison matching Java's equalsIgnoreCase(),
        # still using constant-time digest to prevent timing attacks.
        return hmac.compare_digest(
            expected.lower(),
            callback.biapay_signature.lower(),
        )

    def process_callback(
        self,
        params: dict,
        verify_signature: bool = True,
    ) -> CallbackPayload:
        """Parse and optionally verify a BIAPAY callback from URL query parameters.

        This is a convenience method that combines parsing and verification.

        Args:
            params: Dictionary of query parameters from the callback URL.
                    Pass `request.GET` (Django) or `request.args` (Flask).
            verify_signature: If True (default), verifies the HMAC signature
                              and raises BIAPaySignatureError if invalid.

        Returns:
            A verified :class:`CallbackPayload` instance.

        Raises:
            BIAPaySignatureError: If verification is enabled and the signature is invalid.

        Example (Django)::

            def payment_callback(request):
                try:
                    payload = client.process_callback(request.GET)
                except BIAPaySignatureError:
                    return HttpResponse(status=403)

                if payload.is_completed:
                    Order.objects.filter(id=payload.order_id).update(status="paid")

                return HttpResponse("OK")

        Example (Flask)::

            @app.route("/payment/callback")
            def payment_callback():
                payload = client.process_callback(request.args)
                if payload.is_completed:
                    # handle success
                    pass
                return "OK"
        """
        callback = CallbackPayload.from_query_params(params)

        if verify_signature:
            if not self.verify_callback_signature(callback):
                raise BIAPaySignatureError(
                    "Callback signature verification failed. "
                    "The request may not have originated from BIAPAY."
                )

        return callback

    # -------------------------------------------------------------------------
    # Private helpers
    # -------------------------------------------------------------------------

    def _validate_payment_params(
        self,
        order_id: str,
        amount: int,
        currency_code: str,
        customer_email: str,
        customer_phone: str,
    ) -> None:
        """Validate required payment parameters before sending to the API."""
        errors = []
        if not order_id:
            errors.append("order_id is required.")
        if amount is None or amount <= 0:
            errors.append("amount must be a positive integer.")
        if not currency_code:
            errors.append("currency_code is required.")
        if not customer_email or "@" not in customer_email:
            errors.append("customer_email must be a valid email address.")
        if not customer_phone:
            errors.append("customer_phone is required.")
        if errors:
            raise BIAPayValidationError(" | ".join(errors))

    def _build_auth_payload(
        self,
        order_id: str,
        amount: int,
        currency_code: str,
        customer_email: str,
        customer_phone: str,
        transaction_id: str,
    ) -> dict:
        """Build the JSON payload for the /api/auth/login endpoint."""
        return {
            "clientId": self.client_id,
            "clientSecret": self.client_secret,
            "clientDetails": {
                "originatedBy": self.originated_by,
            },
            "clientUserDetails": {
                "email": customer_email,
                "mobileNumber": customer_phone,
            },
            "orderDetails": {
                "orderId": order_id,
                "amount": amount,
                "currencyCode": currency_code,
            },
            "transactionDetails": {
                "transactionId": transaction_id,
            },
        }

    def _post(self, endpoint: str, payload: dict) -> dict:
        """Send a POST request to the BIAPAY API and return the JSON response.

        Raises:
            BIAPayConnectionError: On network failure.
            BIAPayAuthError: On 401/403 or credential errors.
            BIAPayError: On other HTTP errors.
        """
        url = f"{self.base_url}{endpoint}"
        try:
            response = self._session.post(url, json=payload, timeout=self.timeout)
        except (ConnectionError, Timeout) as e:
            raise BIAPayConnectionError(
                f"Failed to connect to BIAPAY API at {url}: {e}"
            ) from e
        except RequestException as e:
            raise BIAPayConnectionError(f"Network error: {e}") from e

        return self._handle_response(response)

    def _handle_response(self, response: requests.Response) -> dict:
        """Parse and validate the API response.

        Raises:
            BIAPayAuthError: On authentication failure.
            BIAPayError: On any other API error.
        """
        try:
            data = response.json()
        except ValueError:
            raise BIAPayError(
                f"Invalid JSON response from BIAPAY API. "
                f"Status: {response.status_code}. Body: {response.text[:200]}",
                status_code=response.status_code,
            )

        if response.status_code == 200 and "accessToken" in data:
            return data

        # Handle error responses
        message = data.get("message", "Unknown error from BIAPAY API.")
        status_code = response.status_code or data.get("status", 0)

        if status_code in (401, 403) or "credential" in message.lower():
            raise BIAPayAuthError(
                f"Authentication failed: {message}",
                status_code=status_code,
                response=data,
            )

        raise BIAPayError(
            f"BIAPAY API error: {message}",
            status_code=status_code,
            response=data,
        )

    def _compute_hmac(self, payload: str) -> str:
        """Compute RFC 2104-compliant HMAC-SHA256, Base64-encoded.

        Matches the Java implementation:
            HmacSignatureUtil.calculateRFC2104HMAC(data, clientSecret)

        The output is a Base64 string like:
            "DgHy8MqfrTKhWK6KvXcPQN418ApjfOonOA+4/NMCq1g="

        Returns:
            Base64-encoded HMAC-SHA256 digest string.
        """
        raw_bytes = hmac.new(
            self.client_secret.encode("utf-8"),
            payload.encode("utf-8"),
            hashlib.sha256,
        ).digest()                                      # raw bytes (not hex)
        return base64.b64encode(raw_bytes).decode("utf-8")  # Base64 string

    def __repr__(self):
        server_status = "running" if (self._callback_server and self._callback_server.is_running) else "stopped"
        return f"BIAPay(client_id={self.client_id!r}, base_url={self.base_url!r}, callback_server={server_status})"