"""Main client implementation for OxaPay API."""

import hashlib
import hmac
import types
from typing import Any, TypeVar

import aiohttp
from pydantic import BaseModel

from .exceptions import OxaPayError, OxaPayValidationError
from .models.balance import AccountBalanceResponse
from .models.exchange import (
    SwapCalculateRequest,
    SwapCalculateResponse,
    SwapHistoryRequest,
    SwapHistoryResponse,
    SwapPairsResponse,
    SwapRateRequest,
    SwapRateResponse,
    SwapRequest,
    SwapResponse,
)
from .models.merchant import (
    AcceptedCoinsResponse,
    CreateInvoiceRequest,
    CreateInvoiceResponse,
    CreateStaticWalletRequest,
    CreateStaticWalletResponse,
    CreateWhiteLabelRequest,
    CreateWhiteLabelResponse,
    PaymentHistoryRequest,
    PaymentHistoryResponse,
    PaymentInfoResponse,
    RevokeStaticWalletRequest,
    RevokeStaticWalletResponse,
    StaticWalletListRequest,
    StaticWalletListResponse,
)
from .models.payout import (
    CreatePayoutRequest,
    CreatePayoutResponse,
    PayoutHistoryRequest,
    PayoutHistoryResponse,
    PayoutInfoResponse,
)
from .models.system import (
    NetworksResponse,
    PriceResponse,
    SupportedCurrenciesResponse,
    SupportedFiatsResponse,
)

T = TypeVar("T", bound=BaseModel)


class OxaPayClient:
    """Client for interacting with the OxaPay API.

    Provides methods for all OxaPay API endpoints with proper typing and error handling.
    """

    BASE_URL = "https://api.oxapay.com/v1"
    SUCCESS_RESULT_CODE = 200  # Success status code from API

    def __init__(
        self,
        merchant_api_key: str | None = None,
        payout_api_key: str | None = None,
        general_api_key: str | None = None,
    ) -> None:
        """Initialize the OxaPay client.

        Args:
            merchant_api_key: API key for merchant operations
            payout_api_key: API key for payout operations
            general_api_key: API key for general operations like exchange

        """
        self.merchant_api_key = merchant_api_key
        self.payout_api_key = payout_api_key
        self.general_api_key = general_api_key
        self.session = None

    async def __aenter__(self) -> "OxaPayClient":
        """Enter async context manager.

        Returns:
            The OxaPayClient instance.

        """
        self.session = aiohttp.ClientSession()
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: types.TracebackType | None,
    ) -> None:
        """Exit async context manager.

        Args:
            exc_type: Exception type if an exception was raised
            exc_val: Exception value if an exception was raised
            exc_tb: Exception traceback if an exception was raised

        """
        if self.session:
            await self.session.close()
            self.session = None

    async def _request(
        self,
        method: str,
        endpoint: str,
        data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        response_model: type[T] | None = None,
    ) -> T | dict[str, Any]:
        """Make a request to the OxaPay API.

        Args:
            method: HTTP method (GET, POST)
            endpoint: API endpoint path
            data: Request data
            params: Query parameters
            headers: Custom headers to send
            response_model: Pydantic model for response validation

        Returns:
            Validated response data or raw response

        Raises:
            OxaPayError: For API errors
            OxaPayValidationError: For validation errors

        """
        url = f"{self.BASE_URL}{endpoint}"

        if self.session is None:
            self.session = aiohttp.ClientSession()
            close_after = True
        else:
            close_after = False

        # Set default headers
        request_headers = {"Content-Type": "application/json"}
        if headers:
            request_headers.update(headers)

        try:
            async with self.session.request(
                method,
                url,
                json=data,
                params=params,
                headers=request_headers,
            ) as response:
                response_data = await response.json()

                # Check for API errors
                # v1 API uses 'status' field in the body, typically 200 for success
                if (
                    "status" in response_data
                    and response_data["status"] != self.SUCCESS_RESULT_CODE
                ):
                    error_msg = response_data.get("message", "Unknown error")
                    if "error" in response_data and response_data["error"]:
                        error_details = response_data["error"]
                        if (
                            isinstance(error_details, dict)
                            and "message" in error_details
                        ):
                            error_msg = f"{error_msg}: {error_details['message']}"

                    raise OxaPayError(
                        code=response_data.get("status", 0),
                        message=error_msg,
                    )

                # Validate and return the response
                if response_model:
                    try:
                        return response_model.model_validate(response_data)
                    except Exception as e:
                        msg = f"Response validation error: {e!s}"
                        raise OxaPayValidationError(msg) from e
                return response_data
        finally:
            if close_after:
                await self.session.close()
                self.session = None

    def verify_webhook_signature(
        self,
        signature: str,
        payload: str,
        *,
        is_payment: bool = True,
    ) -> bool:
        """Verify webhook signature.

        Args:
            signature: The signature header value
            payload: Raw request body
            is_payment: True for payment/merchant webhooks, False for payout webhooks

        Returns:
            bool: True if signature is valid

        """
        api_key = self.merchant_api_key if is_payment else self.payout_api_key
        if not api_key:
            key_type = "Merchant" if is_payment else "Payout"
            msg = f"{key_type} API key is required"
            raise ValueError(msg)

        calculated_hmac = hmac.new(
            api_key.encode(),
            payload.encode(),
            hashlib.sha512,
        ).hexdigest()

        return calculated_hmac == signature

    # Merchant API Methods

    async def create_invoice(
        self,
        request: CreateInvoiceRequest,
    ) -> CreateInvoiceResponse:
        """Create a new payment invoice.

        Args:
            request: Invoice creation parameters

        Returns:
            Invoice creation response

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        request_data = request.model_dump(by_alias=True, exclude_none=True)

        return await self._request(
            "POST",
            "/payment/invoice",
            data=request_data,
            headers={"merchant_api_key": self.merchant_api_key},
            response_model=CreateInvoiceResponse,
        )

    async def create_white_label(
        self,
        request: CreateWhiteLabelRequest,
    ) -> CreateWhiteLabelResponse:
        """Create a white-label payment.

        Args:
            request: White-label payment request parameters

        Returns:
            White-label payment response

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        request_data = request.model_dump(by_alias=True, exclude_none=True)
        request_data["merchant"] = self.merchant_api_key

        return await self._request(
            "POST",
            "/payment/white-label",
            data=request_data,
            response_model=CreateWhiteLabelResponse,
        )

    async def create_static_wallet(
        self,
        request: CreateStaticWalletRequest,
    ) -> CreateStaticWalletResponse:
        """Create a static wallet address.

        Args:
            request: Static wallet creation parameters

        Returns:
            Static wallet creation response

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        request_data = request.model_dump(by_alias=True, exclude_none=True)
        request_data["merchant"] = self.merchant_api_key

        return await self._request(
            "POST",
            "/payment/static-address",
            data=request_data,
            response_model=CreateStaticWalletResponse,
        )

    async def revoke_static_wallet(
        self,
        request: RevokeStaticWalletRequest,
    ) -> RevokeStaticWalletResponse:
        """Revoke a static wallet address.

        Args:
            request: Revocation parameters

        Returns:
            Revocation response

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        request_data = request.model_dump(by_alias=True, exclude_none=True)
        request_data["merchant"] = self.merchant_api_key

        return await self._request(
            "POST",
            "/payment/revoke/static-address",
            data=request_data,
            response_model=RevokeStaticWalletResponse,
        )

    async def get_static_wallets(
        self,
        request: StaticWalletListRequest,
    ) -> StaticWalletListResponse:
        """List static wallets.

        Args:
            request: List filter parameters

        Returns:
            List of static wallets

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        params = request.model_dump(by_alias=True, exclude_none=True)
        params["merchant"] = self.merchant_api_key

        return await self._request(
            "GET",
            "/payment/static-address",
            params=params,
            response_model=StaticWalletListResponse,
        )

    async def get_payment_info(self, track_id: str | int) -> PaymentInfoResponse:
        """Get information about a specific payment.

        Args:
            track_id: The payment tracking ID

        Returns:
            Payment information

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        # Assuming the API key is passed as a query param or header?
        # Docs say for GET /payment/{trackId}:
        # No reference to API key in params in current docs reading?
        # Wait. "Merchant API Key" section says "Merchant API Key for /payment/*"
        # Usually checking `data.txt`:
        # Line 801 (Get Payment Info Request): No parameters listed.
        # But authentication usually requires key.
        # Let's check common patterns.
        # Line 323 (White Label Request) has `merchant` param.
        # Line 81 (Request Invoice) has `merchant` param.
        # Line 683 (Static Address List) has `merchant` param ? No, line 683 says `merchant` is required in params.
        # Line 801 doesn't list `merchant`. But it's strange if it's open.
        # Maybe it's not needed if trackId is unique global? But privacy?
        # I will assume `merchant` key might be needed as query param if strict auth, OR it's public.
        # Actually Line 1395 (Payment History) requires `merchant` param.
        # So I will add `merchant` generic param to GET requests if not specified?
        # Let's add it to params.

        return await self._request(
            "GET",
            f"/payment/{track_id}",
            params={"merchant": self.merchant_api_key},
            response_model=PaymentInfoResponse,
        )

    async def get_payment_history(
        self,
        request: PaymentHistoryRequest,
    ) -> PaymentHistoryResponse:
        """Get payment history.

        Args:
            request: History filter parameters

        Returns:
            Payment history

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        params = request.model_dump(by_alias=True, exclude_none=True)
        params["merchant"] = self.merchant_api_key

        return await self._request(
            "GET",
            "/payment/list",
            params=params,
            response_model=PaymentHistoryResponse,
        )

    async def get_accepted_coins(self) -> AcceptedCoinsResponse:
        """Get accepted coins.

        Returns:
            List of accepted coins

        """
        if not self.merchant_api_key:
            msg = "Merchant API key is required"
            raise ValueError(msg)

        # No merchant key required apparently? Docs says line 410.
        # But if it's under Merchant API section...
        # Line 417 "Parameters: None".
        # I'll try without key.
        return await self._request(
            "GET",
            "/payment/accepted-currencies",
            params={"merchant": self.merchant_api_key},
            response_model=AcceptedCoinsResponse,
        )

    # Payout API Methods

    # Payout API Methods

    async def create_payout(self, request: CreatePayoutRequest) -> CreatePayoutResponse:
        """Create a new payout.

        Args:
            request: Payout creation parameters

        Returns:
            Payout creation response with tracking ID

        """
        if not self.payout_api_key:
            msg = "Payout API key is required"
            raise ValueError(msg)

        request_data = request.model_dump(by_alias=True, exclude_none=True)
        request_data["key"] = self.payout_api_key

        return await self._request(
            "POST",
            "/payout",
            data=request_data,
            response_model=CreatePayoutResponse,
        )

    async def get_payout_info(self, track_id: str | int) -> PayoutInfoResponse:
        """Get information about a specific payout.

        Args:
            track_id: The payout tracking ID

        Returns:
            Payout information

        """
        if not self.payout_api_key:
            msg = "Payout API key is required"
            raise ValueError(msg)

        return await self._request(
            "GET",
            f"/payout/{track_id}",
            params={"key": self.payout_api_key},
            response_model=PayoutInfoResponse,
        )

    async def get_payout_history(
        self,
        request: PayoutHistoryRequest,
    ) -> PayoutHistoryResponse:
        """Get payout history.

        Args:
            request: Payout history request parameters

        Returns:
            Payout history with pagination info

        """
        if not self.payout_api_key:
            msg = "Payout API key is required"
            raise ValueError(msg)

        params = request.model_dump(by_alias=True, exclude_none=True)
        params["key"] = self.payout_api_key

        return await self._request(
            "GET",
            "/payout/list",
            params=params,
            response_model=PayoutHistoryResponse,
        )

    async def get_payout_balance(
        self, currency: str | None = None
    ) -> AccountBalanceResponse:
        """Get payout account balance.

        Args:
            currency: Specific currency to get balance for (optional)

        Returns:
            Account balance information

        """
        if not self.payout_api_key:
            msg = "Payout API key is required"
            raise ValueError(msg)

        params = {"key": self.payout_api_key}
        if currency:
            params["currency"] = currency

        return await self._request(
            "GET",
            "/payout/balance",
            params=params,
            response_model=AccountBalanceResponse,
        )

    # General/Swap API Methods

    async def get_account_balance(
        self,
        currency: str | None = None,
    ) -> AccountBalanceResponse:
        """Get general account balance.

        Args:
            currency: Specific currency to get balance for (optional)

        Returns:
            Account balance information

        """
        if not self.general_api_key:
            msg = "General API key is required"
            raise ValueError(msg)

        params = {"key": self.general_api_key}
        if currency:
            params["currency"] = currency

        return await self._request(
            "GET",
            "/general/account/balance",
            params=params,
            response_model=AccountBalanceResponse,
        )

    async def get_swap_rate(
        self,
        request: SwapRateRequest,
    ) -> SwapRateResponse:
        """Get swap rate between two currencies.

        Args:
            request: Swap rate request parameters

        Returns:
            Swap rate information

        """
        request_data = request.model_dump(by_alias=True)

        return await self._request(
            "POST",
            "/general/swap/rate",
            data=request_data,
            response_model=SwapRateResponse,
        )

    async def calculate_swap(
        self,
        request: SwapCalculateRequest,
    ) -> SwapCalculateResponse:
        """Calculate swap amount.

        Args:
            request: Swap calculation request parameters

        Returns:
            Swap calculation result

        """
        request_data = request.model_dump(by_alias=True)

        return await self._request(
            "POST",
            "/general/swap/calculate",
            data=request_data,
            response_model=SwapCalculateResponse,
        )

    async def get_swap_pairs(self) -> SwapPairsResponse:
        """Get available swap pairs.

        Returns:
            List of available swap pairs

        """
        return await self._request(
            "GET",
            "/general/swap/pairs",
            response_model=SwapPairsResponse,
        )

    async def create_swap_request(
        self,
        request: SwapRequest,
    ) -> SwapResponse:
        """Create a new swap request.

        Args:
            request: Swap request parameters

        Returns:
            Swap request creation response

        """
        if not self.general_api_key:
            msg = "General API key is required"
            raise ValueError(msg)

        request_data = request.model_dump(by_alias=True, exclude_none=True)
        request_data["key"] = self.general_api_key

        return await self._request(
            "POST",
            "/general/swap",
            data=request_data,
            response_model=SwapResponse,
        )

    async def get_swap_history(
        self,
        request: SwapHistoryRequest,
    ) -> SwapHistoryResponse:
        """Get swap history.

        Args:
            request: Swap history request parameters

        Returns:
            Swap history with pagination info

        """
        if not self.general_api_key:
            msg = "General API key is required"
            raise ValueError(msg)

        params = request.model_dump(by_alias=True, exclude_none=True)
        params["key"] = self.general_api_key

        return await self._request(
            "GET",
            "/general/swap",
            params=params,
            response_model=SwapHistoryResponse,
        )

    # System/Common API Methods

    async def get_prices(self) -> PriceResponse:
        """Get current prices of all supported cryptocurrencies.

        Returns:
            Current prices information

        """
        return await self._request(
            "GET", "/common/prices", response_model=PriceResponse
        )

    async def get_supported_currencies(self) -> SupportedCurrenciesResponse:
        """Get a list of supported currencies.

        Returns:
            List of supported currencies with network details

        """
        return await self._request(
            "GET",
            "/common/currencies",
            response_model=SupportedCurrenciesResponse,
        )

    async def get_supported_fiats(self) -> SupportedFiatsResponse:
        """Get a list of supported fiat currencies.

        Returns:
            List of supported fiat currencies

        """
        return await self._request(
            "GET",
            "/common/fiats",
            response_model=SupportedFiatsResponse,
        )

    async def get_networks(self) -> NetworksResponse:
        """Get a list of supported blockchain networks.

        Returns:
            List of supported networks

        """
        return await self._request(
            "GET",
            "/common/networks",
            response_model=NetworksResponse,
        )

    async def check_system_status(self) -> str:
        """Check if the OxaPay API is operational.

        Returns:
            Status message (usually "OK")

        """
        response = await self._request("GET", "/common/monitor")
        # According to docs: "message": "System is running..."
        if (
            isinstance(response, dict)
            and response.get("message") == "System is running smoothly."
        ):
            return "OK"
        if isinstance(response, dict) and "message" in response:
            return response["message"]
        return str(response)
