import abc
from datetime import datetime
from typing import Any, Optional, List, Union

from typing_extensions import override

from ._base import NotchPayBaseResource, AsyncNotchPayBaseResource
from .exceptions import NotchPayError
from .schemas import NotchPayResponseList, NotchPayChannels, NotchPayResponse
from .schemas.base import MaybeAwaitable
from .schemas.payments import NotchPayPayment, NotchPayPaymentCreate, NotchPayPaymentCreateResponse, \
    NotchPayPaymentResponse, NotchPayProcessPayment, NotchPayProcessPaymentData

__all__ = [
    "NotchPayPayments",
    "AsyncNotchPayPayments",
]


class _NotchPayPayments(abc.ABC):
    """Abstract base class for Notchpay payments management."""

    @abc.abstractmethod
    def list(
            self,
            limit: int = 30,
            page: int = 1,
            search: Optional[str] = None,
            status: Optional[str] = None,
            channels: Optional[List[NotchPayChannels]] = None,
            date_start: Optional[Union[datetime, str]] = None,
            date_end: Optional[Union[datetime, str]] = None,
    ) -> MaybeAwaitable[NotchPayResponseList[NotchPayPayment]]:
        """List all payments.

        Args:
            limit: Number of items per page (default: 30).
            page: Page number (default: 1).
            search: Search by reference.
            status: Filter by status (e.g., "complete", "pending").
            channels: Filter by payment channels.
            date_start: Start date or `str` in YYYY-MM-DD format.
            date_end: End date or `str` in YYYY-MM-DD format.

        Returns:
            NotchPayResponseList[NotchPayPayment]: Paginated list of payments.

        Raises:
            NotchPayError: In case of API request error.

        References:
            https://developer.notchpay.co/api-reference/list-all-payments

        Example:
            >>> from notchpay import NotchPay
            >>> notchpay = NotchPay("your_api_key")
            >>> payments = notchpay.payments.list(limit=10, status="complete")
            >>> for payment in payments.items:
            ...     print(payment.reference, payment.amount)
        """
        # TODO: Filters by channels not working yet on api
        ...

    @abc.abstractmethod
    def create(self, data: NotchPayPaymentCreate) -> MaybeAwaitable[NotchPayPaymentCreateResponse]:
        """Initialize a new payment.

        Args:
            data: Payment initialization data.

        Returns:
            NotchPayPaymentCreateResponse: Payment initialization response.

        Raises:
            NotchPayError: In case of initialization error.

        References:
            https://developer.notchpay.co/api-reference/initialize-a-payment

        Example:
            >>> from notchpay import NotchPay
            >>> notchpay = NotchPay("your_api_key")
            >>> payment = notchpay.payments.create(
            ...     NotchPayPaymentCreate(
            ...         amount=5000,
            ...         currency="XAF",
            ...         email="customer@example.com",
            ...         description="Product purchase"
            ...     )
            ... )
            >>> print(payment.authorization_url)
        """
        ...

    @abc.abstractmethod
    def retrieve(self, reference: str) -> MaybeAwaitable[NotchPayPaymentResponse]:
        """Retrieve payment details.

        Args:
            reference: Unique payment reference.

        Returns:
            NotchPayPaymentResponse: Payment response.

        Raises:
            NotchPayError: If payment doesn't exist or in case of API error.

        References:
            https://developer.notchpay.co/api-reference/retrieve-a-payment

        Example:
            >>> from notchpay import NotchPay
            >>> notchpay = NotchPay("your_api_key")
            >>> payment = notchpay.payments.retrieve("pay_123456789")
            >>> print(payment.transaction.status)
        """
        ...

    @abc.abstractmethod
    def cancel(self, reference: str) -> MaybeAwaitable[NotchPayResponse]:
        """Cancel a payment.

        Args:
            reference: Unique reference of the payment to cancel.

        Returns:
            NotchPayResponse: Cancellation response.

        Raises:
            NotchPayError: If payment cannot be cancelled.

        References:
            https://developer.notchpay.co/api-reference/cancel-a-payment

        Example:
            >>> from notchpay import NotchPay
            >>> notchpay = NotchPay("your_api_key")
            >>> payment = notchpay.payments.cancel("pay_123456789")
            >>> print(payment.transaction.status)
        """
        ...

    @abc.abstractmethod
    def process(self, reference: str, data: NotchPayProcessPayment) -> MaybeAwaitable[NotchPayPaymentResponse]:
        """Process a payment with a specific channel.

        Args:
            reference: Unique payment reference.
            data: Payment processing data.

        Returns:
            NotchPayPaymentResponse: Processing response.

        Raises:
            NotchPayError: In case of processing error.

        References:
            https://developer.notchpay.co/api-reference/process-a-payment-1

        Example:
            >>> from notchpay import NotchPay
            >>> notchpay = NotchPay("your_api_key")
            >>> payment = notchpay.payments.process(
            ...     "pay_123456789",
            ...     NotchPayProcessPayment(
            ...         channel=NotchPayChannels.CM_MTN,
            ...         data=NotchPayProcessPaymentData(
            ...             phone="+237680000000"
            ...         )
            ...     )
            ...)
            >>> print(payment.transaction.status)
        """
        ...

    @abc.abstractmethod
    def direct_charge(
            self,
            data: NotchPayPaymentCreate,
            channel: NotchPayChannels,
            phone: Optional[str] = None,
    ) -> MaybeAwaitable[NotchPayPaymentResponse]:
        """Create and process a payment in a single operation.

        Args:
            data: Payment initialization data.
            channel: Payment channel to use.
            phone: Phone number for payment (optional if already in data).

        Returns:
            NotchPayPaymentResponse: Processed payment response.

        Raises:
            NotchPayError: In case of creation or processing error.

        References:
            https://developer.notchpay.co/accept-payments/charge

        Example:
            >>> from notchpay import NotchPay
            >>> from notchpay.schemas import NotchPayChannels
            >>> notchpay = NotchPay("your_api_key")
            >>> payment = notchpay.payments.direct_charge(
            ...     NotchPayPaymentCreate(
            ...         amount=5000,
            ...         currency="XAF",
            ...         email="customer@example.com",
            ...         description="Product purchase"
            ...     ),
            ...     channel=NotchPayChannels.CM_MTN,
            ...     phone="+237680000000"
            ... )
            >>> print(payment.transaction.status)
        """
        ...

    def _build_list_params(
            self,
            limit: int = 30,
            page: int = 1,
            search: Optional[str] = None,
            status: Optional[str] = None,
            channels: Optional[List[NotchPayChannels]] = None,
            date_start: Optional[Union[datetime, str]] = None,
            date_end: Optional[Union[datetime, str]] = None,
    ) -> dict[str, Any]:
        """Build query parameters for payments list.

        Args:
            limit: Number of items per page.
            page: Page number.
            search: Search by reference.
            status: Filter by status.
            channels: Filter by channels.
            date_start: Start date.
            date_end: End date.

        Returns:
            dict: Dictionary of query parameters.
        """
        params: dict[str, Any] = {"limit": limit, "page": page}
        if search is not None:
            params["search"] = search
        if status is not None:
            params["status"] = status
        if channels is not None:
            params["channels"] = ",".join([str(channel.value) for channel in channels])
        if date_start is not None:
            params["date_start"] = date_start.strftime("%Y-%m-%d") if isinstance(date_start, datetime) else date_start
        if date_end is not None:
            params["date_end"] = date_end.strftime("%Y-%m-%d") if isinstance(date_end, datetime) else date_end

        return {"params": params}

    def _check_create_params(self, data: NotchPayPaymentCreate) -> None:
        if data.get("customer") is None and data.get("email") is None and data.get("phone") is None:
            raise NotchPayError("Customer or email or phone is required")

    def _prepare_direct_charge_data(
            self,
            data: NotchPayPaymentCreate,
            phone: Optional[str] = None,
    ) -> NotchPayPaymentCreate:
        if phone is None and data.get('phone') is None:
            raise NotchPayError("Phone number is required for direct charge")
        if phone is not None and not data.get("phone"):
            data["phone"] = phone
        return data


class NotchPayPayments(_NotchPayPayments, NotchPayBaseResource):
    """Synchronous client for Notchpay payments management.

    This class allows performing all payment-related operations
    synchronously.

    Example:
        >>> from notchpay import NotchPay
        >>> notchpay = NotchPay("your_api_key")
        >>> payments = notchpay.payments.list(limit=5)
        >>> print(f"Number of payments: {len(payments.items)}")
    """

    def list(
            self,
            limit: int = 30,
            page: int = 1,
            search: Optional[str] = None,
            status: Optional[str] = None,
            channels: Optional[list[NotchPayChannels]] = None,
            date_start: Optional[Union[str, datetime]] = None,
            date_end: Optional[Union[str, datetime]] = None,
    ) -> NotchPayResponseList[NotchPayPayment]:
        params = super()._build_list_params(limit, page, search, status, channels, date_start, date_end)
        return self._request(
            "GET",
            "/payments",
            response_model=NotchPayResponseList[NotchPayPayment],
            **params
        )

    def create(self, data: NotchPayPaymentCreate) -> NotchPayPaymentCreateResponse:
        self._check_create_params(data)
        return self._request(
            "POST",
            "/payments",
            response_model=NotchPayPaymentCreateResponse,
            json=data
        )

    def cancel(self, reference: str) -> NotchPayResponse:
        return self._request(
            "DELETE",
            f"/payments/{reference}",
            response_model=NotchPayResponse,
        )

    def retrieve(self, reference: str) -> NotchPayPaymentResponse:
        return self._request(
            "GET",
            f"/payments/{reference}",
            response_model=NotchPayPaymentResponse,
        )

    def process(self, reference: str, data: NotchPayProcessPayment) -> NotchPayPaymentResponse:
        return self._request(
            "POST",
            f"/payments/{reference}",
            response_model=NotchPayPaymentResponse,
            json=data
        )

    def direct_charge(
            self,
            data: NotchPayPaymentCreate,
            channel: NotchPayChannels,
            phone: Optional[str] = None,
    ) -> NotchPayPaymentResponse:
        charge_data = self._prepare_direct_charge_data(data, phone)

        create_response = self.create(charge_data)
        reference = create_response.transaction.reference

        process_phone = phone or charge_data.get("phone")
        if not process_phone:
            raise NotchPayError("Phone number is required for direct charge")

        process_data = NotchPayProcessPayment(
            channel=channel,
            data=NotchPayProcessPaymentData(phone=process_phone)
        )

        return self.process(reference, process_data)


class AsyncNotchPayPayments(_NotchPayPayments, AsyncNotchPayBaseResource):
    """Asynchronous client for Notchpay payments management.

    This class allows performing all payment-related operations
    asynchronously.

    Example:
        >>> import asyncio
        >>> from notchpay import AsyncNotchPay
        >>> async def main():
        ...    notchpay = AsyncNotchPay("your_api_key")
        ...    payments = await notchpay.payments.list(limit=5)
        ...    print(f"Number of payments: {len(payments.items)}")
        >>> asyncio.run(main())
    """

    @override
    async def list(
            self,
            limit: int = 30,
            page: int = 1,
            search: Optional[str] = None,
            status: Optional[str] = None,
            channels: Optional[list[NotchPayChannels]] = None,
            date_start: Optional[Union[str, datetime]] = None,
            date_end: Optional[Union[str, datetime]] = None,
    ) -> NotchPayResponseList[NotchPayPayment]:
        params = super()._build_list_params(limit, page, search, status, channels, date_start, date_end)
        return await self._request(
            "GET",
            "/payments",
            response_model=NotchPayResponseList[NotchPayPayment],
            **params
        )

    async def create(self, data: NotchPayPaymentCreate) -> NotchPayPaymentCreateResponse:
        self._check_create_params(data)
        return await self._request(
            "POST",
            "/payments",
            response_model=NotchPayPaymentCreateResponse,
            json=data
        )

    async def cancel(self, reference: str) -> NotchPayResponse:
        return await self._request(
            "DELETE",
            f"/payments/{reference}",
            response_model=NotchPayResponse,
        )

    async def retrieve(self, reference: str) -> NotchPayPaymentResponse:
        return await self._request(
            "GET",
            f"/payments/{reference}",
            response_model=NotchPayPaymentResponse,
        )

    async def process(self, reference: str, data: NotchPayProcessPayment) -> NotchPayPaymentResponse:
        return await self._request(
            "POST",
            f"/payments/{reference}",
            response_model=NotchPayPaymentResponse,
            json=data
        )

    async def direct_charge(
            self,
            data: NotchPayPaymentCreate,
            channel: NotchPayChannels,
            phone: Optional[str] = None,
    ) -> NotchPayPaymentResponse:
        charge_data = self._prepare_direct_charge_data(data, phone)

        create_response = await self.create(charge_data)
        reference = create_response.transaction.reference

        process_phone = phone or charge_data.get("phone")
        if not process_phone:
            raise NotchPayError("Phone number is required for direct charge")

        process_data = NotchPayProcessPayment(
            channel=channel,
            data=NotchPayProcessPaymentData(phone=process_phone)
        )

        return await self.process(reference, process_data)
