"""Pass payment client implementations (sync and async)."""

import re
from typing import Any, Dict, List, Optional
from urllib.parse import urlencode

import httpx

from .errors import PassError
from .types import (
    CustomerData,
    Datum,
    ExtraData,
    LoyaltyData,
    PassConfig,
    RetCreateOrder,
    RetInquiryOrder,
    RetNotifyOrder,
    RetOrderCancel,
    RetOrderVoid,
    WebhookCallbackResponse,
)


def _parse_customer_data(data: Optional[Dict[str, Any]]) -> Optional[CustomerData]:
    """Parse customer data dict into a CustomerData dataclass."""
    if not data:
        return None
    return CustomerData(
        user_id=data.get("user_id", ""),
        unique_id=data.get("unique_id", ""),
    )


def _parse_loyalty_data(data: Optional[Dict[str, Any]]) -> Optional[LoyaltyData]:
    """Parse loyalty data dict into a LoyaltyData dataclass."""
    if not data:
        return None
    return LoyaltyData(
        kb_status=data.get("kb_status", ""),
        kb_txn_id=data.get("kb_txn_id", ""),
        kb_card_id=data.get("kb_card_id", ""),
        kb_usable_lp=data.get("kb_usable_lp", ""),
        kb_loyalty_pk=data.get("kb_loyalty_pk", ""),
        has_kb_loyalty=data.get("has_kb_loyalty", ""),
        kb_description=data.get("kb_description", ""),
        kb_limit_value=data.get("kb_limit_value", ""),
        kb_loyalty_type=data.get("kb_loyalty_type", ""),
        kb_dates_of_week=data.get("kb_dates_of_week", ""),
        kb_no_txn_amount=data.get("kb_no_txn_amount", ""),
        kb_yes_txn_amount=data.get("kb_yes_txn_amount", ""),
        kb_loyalty_provider_name=data.get("kb_loyalty_provider_name", ""),
    )


def _parse_extra_data(data: Optional[Dict[str, Any]]) -> Optional[ExtraData]:
    """Parse extra data dict into an ExtraData dataclass."""
    if not data:
        return None
    return ExtraData(
        _id=data.get("_id", ""),
        pan=data.get("pan", ""),
        rrn=data.get("rrn", ""),
        amount=data.get("amount", ""),
        pos_id=data.get("pos_id", ""),
        order_id=data.get("order_id", ""),
        resp_msg=data.get("resp_msg", ""),
        trace_no=data.get("trace_no", ""),
        date_time=data.get("date_time", ""),
        resp_code=data.get("resp_code", ""),
        terminal_id=data.get("terminal_id", ""),
        approved_code=data.get("approved_code", ""),
        currency_code=data.get("currency_code", ""),
        payment_request_id=data.get("payment_request_id", ""),
    )


def _build_callback_url(
    base_callback: str, params: Optional[Dict[str, str]]
) -> str:
    """Build a callback URL with optional query parameters appended."""
    if not params:
        return base_callback
    encoded = urlencode(params)
    separator = "&" if "?" in base_callback else "?"
    return f"{base_callback}{separator}{encoded}"


def _check_error(data: Dict[str, Any]) -> None:
    """Check for API-level errors in the response and raise PassError if found."""
    msg = data.get("msg")
    if msg and msg.get("code"):
        raise PassError(
            f"Pass API error ({msg['code']}): {msg.get('body', '')}",
            code=msg.get("code"),
            level=msg.get("level"),
            body=msg.get("body"),
            response=data,
        )


def _parse_create_order(ret: Dict[str, Any]) -> RetCreateOrder:
    """Parse the ret field of a create_order response."""
    return RetCreateOrder(
        shop=ret.get("shop", ""),
        amount=ret.get("amount", ""),
        order_id=ret.get("order_id", ""),
        order_ttl=ret.get("order_ttl", 0),
        db_ref_no=ret.get("db_ref_no", ""),
    )


def _parse_inquiry_order(ret: Dict[str, Any]) -> RetInquiryOrder:
    """Parse the ret field of an inquiry_order response."""
    return RetInquiryOrder(
        resp_code=ret.get("resp_code", ""),
        resp_msg=ret.get("resp_msg", ""),
        status=ret.get("status", ""),
        amount=ret.get("amount", ""),
        db_ref_no=ret.get("db_ref_no", ""),
        status_text=ret.get("status_text", ""),
        customer_data=_parse_customer_data(ret.get("customer_data")),
        loyalty_data=_parse_loyalty_data(ret.get("loyalty_data")),
        extra_data=_parse_extra_data(ret.get("extra_data")),
    )


def _parse_notify_order(ret: Dict[str, Any]) -> RetNotifyOrder:
    """Parse the ret field of a notify_order response."""
    data_list: List[Datum] = []
    for item in ret.get("data", []):
        data_list.append(
            Datum(
                success=item.get("success", False),
                message_id=item.get("message_id", ""),
            )
        )
    return RetNotifyOrder(
        resp_code=ret.get("resp_code", ""),
        resp_msg=ret.get("resp_msg", ""),
        success=ret.get("success", 0),
        data=data_list,
    )


def _parse_order_cancel(ret: Dict[str, Any]) -> RetOrderCancel:
    """Parse the ret field of a cancel_order response."""
    return RetOrderCancel(
        resp_code=ret.get("resp_code", ""),
        resp_msg=ret.get("resp_msg", ""),
        status=ret.get("status", ""),
        amount=ret.get("amount", ""),
        loyalty_data=ret.get("loyalty_data"),
        db_ref_no=ret.get("db_ref_no", ""),
    )


def _parse_order_void(ret: Dict[str, Any]) -> RetOrderVoid:
    """Parse the ret field of a void response."""
    return RetOrderVoid(
        resp_code=ret.get("resp_code", ""),
        resp_msg=ret.get("resp_msg", ""),
        status=ret.get("status", ""),
        amount=ret.get("amount", ""),
        loyalty_data=ret.get("loyalty_data"),
        db_ref_no=ret.get("db_ref_no", ""),
        service_name=ret.get("service_name", ""),
        date_time=ret.get("date_time", ""),
        trace_no=ret.get("trace_no", ""),
        rrn=ret.get("rrn", ""),
        terminal_id=ret.get("terminal_id", ""),
        merchant_id=ret.get("merchant_id", ""),
    )


class PassClient:
    """Pass payment client (synchronous).

    All methods automatically inject the ecommerce_token into every request body,
    so callers never need to deal with token management.
    """

    def __init__(self, config: PassConfig) -> None:
        if not config.endpoint:
            raise ValueError("PassClient: endpoint is required")
        if not config.ecommerce_token:
            raise ValueError("PassClient: ecommerce_token is required")
        if not config.callback:
            raise ValueError("PassClient: callback is required")

        # Strip trailing slashes for consistent URL building
        self._endpoint = re.sub(r"/+$", "", config.endpoint)
        self._ecommerce_token = config.ecommerce_token
        self._callback = config.callback
        self._client = httpx.Client()

    # -- Public API --

    def create_order(
        self,
        amount: float,
        callback_params: Optional[Dict[str, str]] = None,
    ) -> RetCreateOrder:
        """Create a new order.

        Args:
            amount: Order amount (will be multiplied by 100 before sending).
            callback_params: Optional query parameters to append to the callback URL.

        Returns:
            The created order details.
        """
        callback_url = _build_callback_url(self._callback, callback_params)
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "amount": amount * 100,
            "callback_url": callback_url,
        }

        data = self._post("/create_order", body)
        _check_error(data)
        return _parse_create_order(data.get("ret", {}))

    def inquiry_order(self, order_id: str) -> RetInquiryOrder:
        """Inquire about an order's status.

        Args:
            order_id: The order ID to inquire about.

        Returns:
            The order inquiry result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
        }

        data = self._post("/order_inquiry", body)
        _check_error(data)
        return _parse_inquiry_order(data.get("ret", {}))

    def notify_order(self, order_id: str, phone: str) -> RetNotifyOrder:
        """Send a notification for an order to a phone number.

        Args:
            order_id: The order ID.
            phone: The phone number to notify.

        Returns:
            The notification result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
            "phone": phone,
        }

        data = self._post("/order_notify", body)
        _check_error(data)
        return _parse_notify_order(data.get("ret", {}))

    def cancel_order(self, order_id: str) -> RetOrderCancel:
        """Cancel an order.

        Args:
            order_id: The order ID to cancel.

        Returns:
            The cancellation result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
        }

        data = self._post("/cancel_order", body)
        _check_error(data)
        return _parse_order_cancel(data.get("ret", {}))

    def void_order(self, order_id: str) -> RetOrderVoid:
        """Void an order.

        Args:
            order_id: The order ID to void.

        Returns:
            The void result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
        }

        data = self._post("/void", body)
        _check_error(data)
        return _parse_order_void(data.get("ret", {}))

    @staticmethod
    def parse_webhook(body: Any) -> WebhookCallbackResponse:
        """Parse and validate a webhook callback payload.

        Args:
            body: The raw webhook request body (dict).

        Returns:
            The parsed webhook response.

        Raises:
            PassError: If the payload is invalid.
        """
        if not body or not isinstance(body, dict):
            raise PassError("Invalid webhook payload: expected an object")

        if not isinstance(body.get("order_id"), str) or not body.get("order_id"):
            raise PassError(
                "Invalid webhook payload: missing or invalid order_id"
            )

        return WebhookCallbackResponse(
            order_id=body["order_id"],
            payment_request_id=body.get("payment_request_id", ""),
            pos_id=body.get("pos_id", ""),
            operation=body.get("operation", ""),
            is_success=body.get("is_success", False),
            amount=body.get("amount", ""),
            created_time=body.get("created_time", ""),
            db_ref_no=body.get("db_ref_no", ""),
            customer_data=_parse_customer_data(body.get("customer_data")),
            extra_data=body.get("extra_data"),
        )

    def close(self) -> None:
        """Close the underlying HTTP client."""
        self._client.close()

    def __enter__(self) -> "PassClient":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    # -- Private helpers --

    def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
        """Send a POST request to the Pass API."""
        url = f"{self._endpoint}{path}"

        try:
            res = self._client.post(
                url,
                json=body,
                headers={"Content-Type": "application/json"},
            )
        except httpx.HTTPError as exc:
            raise PassError(
                f"Network error calling {path}: {exc}"
            ) from exc

        try:
            json_data: Dict[str, Any] = res.json()
        except Exception:
            raise PassError(
                f"Invalid JSON response from {path}",
                status_code=res.status_code,
            )

        if res.status_code < 200 or res.status_code >= 300:
            raise PassError(
                f"Pass API error: HTTP {res.status_code}",
                status_code=res.status_code,
                response=json_data,
            )

        return json_data


class AsyncPassClient:
    """Pass payment client (asynchronous).

    All methods automatically inject the ecommerce_token into every request body,
    so callers never need to deal with token management.
    """

    def __init__(self, config: PassConfig) -> None:
        if not config.endpoint:
            raise ValueError("AsyncPassClient: endpoint is required")
        if not config.ecommerce_token:
            raise ValueError("AsyncPassClient: ecommerce_token is required")
        if not config.callback:
            raise ValueError("AsyncPassClient: callback is required")

        # Strip trailing slashes for consistent URL building
        self._endpoint = re.sub(r"/+$", "", config.endpoint)
        self._ecommerce_token = config.ecommerce_token
        self._callback = config.callback
        self._client = httpx.AsyncClient()

    # -- Public API --

    async def create_order(
        self,
        amount: float,
        callback_params: Optional[Dict[str, str]] = None,
    ) -> RetCreateOrder:
        """Create a new order.

        Args:
            amount: Order amount (will be multiplied by 100 before sending).
            callback_params: Optional query parameters to append to the callback URL.

        Returns:
            The created order details.
        """
        callback_url = _build_callback_url(self._callback, callback_params)
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "amount": amount * 100,
            "callback_url": callback_url,
        }

        data = await self._post("/create_order", body)
        _check_error(data)
        return _parse_create_order(data.get("ret", {}))

    async def inquiry_order(self, order_id: str) -> RetInquiryOrder:
        """Inquire about an order's status.

        Args:
            order_id: The order ID to inquire about.

        Returns:
            The order inquiry result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
        }

        data = await self._post("/order_inquiry", body)
        _check_error(data)
        return _parse_inquiry_order(data.get("ret", {}))

    async def notify_order(self, order_id: str, phone: str) -> RetNotifyOrder:
        """Send a notification for an order to a phone number.

        Args:
            order_id: The order ID.
            phone: The phone number to notify.

        Returns:
            The notification result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
            "phone": phone,
        }

        data = await self._post("/order_notify", body)
        _check_error(data)
        return _parse_notify_order(data.get("ret", {}))

    async def cancel_order(self, order_id: str) -> RetOrderCancel:
        """Cancel an order.

        Args:
            order_id: The order ID to cancel.

        Returns:
            The cancellation result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
        }

        data = await self._post("/cancel_order", body)
        _check_error(data)
        return _parse_order_cancel(data.get("ret", {}))

    async def void_order(self, order_id: str) -> RetOrderVoid:
        """Void an order.

        Args:
            order_id: The order ID to void.

        Returns:
            The void result.
        """
        body: Dict[str, Any] = {
            "ecommerce_token": self._ecommerce_token,
            "order_id": order_id,
        }

        data = await self._post("/void", body)
        _check_error(data)
        return _parse_order_void(data.get("ret", {}))

    @staticmethod
    def parse_webhook(body: Any) -> WebhookCallbackResponse:
        """Parse and validate a webhook callback payload.

        Args:
            body: The raw webhook request body (dict).

        Returns:
            The parsed webhook response.

        Raises:
            PassError: If the payload is invalid.
        """
        return PassClient.parse_webhook(body)

    async def close(self) -> None:
        """Close the underlying HTTP client."""
        await self._client.aclose()

    async def __aenter__(self) -> "AsyncPassClient":
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    # -- Private helpers --

    async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
        """Send a POST request to the Pass API."""
        url = f"{self._endpoint}{path}"

        try:
            res = await self._client.post(
                url,
                json=body,
                headers={"Content-Type": "application/json"},
            )
        except httpx.HTTPError as exc:
            raise PassError(
                f"Network error calling {path}: {exc}"
            ) from exc

        try:
            json_data: Dict[str, Any] = res.json()
        except Exception:
            raise PassError(
                f"Invalid JSON response from {path}",
                status_code=res.status_code,
            )

        if res.status_code < 200 or res.status_code >= 300:
            raise PassError(
                f"Pass API error: HTTP {res.status_code}",
                status_code=res.status_code,
                response=json_data,
            )

        return json_data
