"""
BIAPAY SDK Client
"""

import hashlib
import hmac
import base64
from typing import Optional

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,
)

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",
        })

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

    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):
        return f"BIAPay(client_id={self.client_id!r}, base_url={self.base_url!r})"