"""Async client for PayPalych (Pal24) API."""

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from typing import Any

import aiohttp
import structlog

from app.config import settings


logger = structlog.get_logger(__name__)


class Pal24APIError(Exception):
    """Base error for Pal24 API operations."""


@dataclass(slots=True)
class Pal24Response:
    """Wrapper for Pal24 API responses."""

    success: bool
    data: dict[str, Any]
    status: int

    @classmethod
    def from_payload(cls, payload: dict[str, Any], status: int) -> Pal24Response:
        success = bool(payload.get('success', status < 400))
        return cls(success=success, data=payload, status=status)

    def raise_for_status(self, endpoint: str) -> None:
        if not self.success:
            detail = self.data.get('message') or self.data.get('error')
            raise Pal24APIError(f'Pal24 API error at {endpoint}: status={self.status}, detail={detail or self.data}')


class Pal24Client:
    """Async client implementing PayPalych API methods."""

    def __init__(
        self,
        *,
        api_token: str | None = None,
        base_url: str | None = None,
        timeout: int | None = None,
    ) -> None:
        self.api_token = api_token or settings.PAL24_API_TOKEN
        self.base_url = (base_url or settings.PAL24_BASE_URL or '').rstrip('/') + '/'
        self.timeout = timeout or settings.PAL24_REQUEST_TIMEOUT

        if not self.api_token:
            logger.warning('Pal24Client initialized without API token')

    @property
    def is_configured(self) -> bool:
        return bool(self.api_token and self.base_url)

    async def _request(
        self,
        method: str,
        endpoint: str,
        *,
        json_payload: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> Pal24Response:
        if not self.is_configured:
            raise Pal24APIError('Pal24 client is not configured')

        url = f'{self.base_url}{endpoint.lstrip("/")}'
        headers = {
            'Authorization': f'Bearer {self.api_token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        }

        timeout = aiohttp.ClientTimeout(total=self.timeout)

        try:
            async with (
                aiohttp.ClientSession(timeout=timeout) as session,
                session.request(
                    method,
                    url,
                    headers=headers,
                    json=json_payload,
                    params=params,
                ) as response,
            ):
                status = response.status
                try:
                    payload = await response.json(content_type=None)
                except aiohttp.ContentTypeError:
                    text_body = await response.text()
                    logger.error('Pal24 API returned non-JSON response for', endpoint=endpoint, text_body=text_body)
                    raise Pal24APIError(f'Pal24 API returned non-JSON response: {text_body}') from None

                result = Pal24Response.from_payload(payload, status)
                if status >= 400 or not result.success:
                    logger.error('Pal24 API error', status=status, endpoint=endpoint, payload=payload)
                    result.raise_for_status(endpoint)

                return result

        except TimeoutError as error:
            logger.error('Pal24 API request timeout for', endpoint=endpoint, error=error)
            raise Pal24APIError(f'Pal24 API request timeout for {endpoint}') from error
        except aiohttp.ClientError as error:
            logger.error('Pal24 API client error for', endpoint=endpoint, error=error)
            raise Pal24APIError(str(error)) from error

    # API methods -----------------------------------------------------------------

    async def create_bill(
        self,
        *,
        amount: Decimal,
        shop_id: str,
        order_id: str | None = None,
        description: str | None = None,
        currency_in: str = 'RUB',
        type_: str = 'normal',
        **kwargs: Any,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {
            'amount': str(amount),
            'shop_id': shop_id,
            'currency_in': currency_in,
            'type': type_,
        }

        if order_id:
            payload['order_id'] = order_id
        if description:
            payload['description'] = description

        payload.update({k: v for k, v in kwargs.items() if v is not None})

        response = await self._request('POST', 'bill/create', json_payload=payload)
        return response.data

    async def get_bill_status(self, bill_id: str) -> dict[str, Any]:
        response = await self._request('GET', 'bill/status', params={'id': bill_id})
        return response.data

    async def toggle_bill_activity(self, bill_id: str, active: bool) -> dict[str, Any]:
        payload = {'id': bill_id, 'active': 1 if active else 0}
        response = await self._request('POST', 'bill/toggle_activity', json_payload=payload)
        return response.data

    async def search_payments(self, **params: Any) -> dict[str, Any]:
        response = await self._request('GET', 'payment/search', params=params)
        return response.data

    async def get_payment_status(self, payment_id: str) -> dict[str, Any]:
        response = await self._request('GET', 'payment/status', params={'id': payment_id})
        return response.data

    async def get_balance(self) -> dict[str, Any]:
        response = await self._request('GET', 'merchant/balance')
        return response.data

    async def search_bills(self, **params: Any) -> dict[str, Any]:
        response = await self._request('GET', 'bill/search', params=params)
        return response.data

    async def get_bill_payments(self, bill_id: str) -> dict[str, Any]:
        response = await self._request('GET', 'bill/payments', params={'id': bill_id})
        return response.data

    # Helpers ---------------------------------------------------------------------

    @staticmethod
    def calculate_signature(out_sum: str, inv_id: str, api_token: str | None = None) -> str:
        token = api_token or settings.PAL24_SIGNATURE_TOKEN or settings.PAL24_API_TOKEN
        if not token:
            raise Pal24APIError('Pal24 signature token is not configured')
        raw = f'{out_sum}:{inv_id}:{token}'.encode()
        return hashlib.md5(raw).hexdigest().upper()

    @staticmethod
    def verify_signature(
        out_sum: str,
        inv_id: str,
        signature: str,
        api_token: str | None = None,
    ) -> bool:
        try:
            expected = Pal24Client.calculate_signature(out_sum, inv_id, api_token)
        except Pal24APIError:
            logger.error('Pal24 signature verification failed: missing token')
            return False
        return expected == signature.upper()

    @staticmethod
    def normalize_amount(amount_kopeks: int) -> Decimal:
        try:
            return (Decimal(amount_kopeks) / Decimal(100)).quantize(Decimal('0.01'))
        except (InvalidOperation, TypeError) as error:
            raise Pal24APIError(f'Invalid amount: {amount_kopeks}') from error
