"""HTTP client for the Garanti BBVA Switch (Card Storage) JSON API.

Provides :class:`SwitchClient` for card tokenization and token-based
recurring payments. This is a separate system from the XML-based
VPServlet API handled by :class:`~garantipay.client.GarantiClient`.

Typical subscription/recurring payment flow::

    from garantipay import SwitchClient

    client = SwitchClient(
        swt_id="YOUR_SWITCH_ID",
        swt_password="YOUR_SWITCH_PASSWORD",
        user_id="YOUR_USER_ID",
        mode="TEST",
    )

    # Step 1: Store the card (first time only)
    token_response = client.generate_token(
        card_number="5549601634451019",
        expire_month="02",
        expire_year="25",
    )
    token = token_response.card.token  # Save this in your database

    # Step 2: Charge monthly using the stored token
    payment = client.sale_with_token(
        token=token,
        amount="10.50",
        currency_code="949",
        customer_ip="192.168.1.1",
    )

    # Step 3: When subscription ends, delete the token
    client.delete_token(token)

Reference:
    https://dev.garantibbva.com.tr/kart-saklama-api-istegi-ile-token-alma
    https://dev.garantibbva.com.tr/en/switch-sales-tokenized-non-3ds
"""

import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, Optional

import requests as http_requests

from garantipay.constants import CURRENCIES
from garantipay.exceptions import (
    GarantiPayConnectionError,
    GarantiPayValidationError,
)
from garantipay.switch_constants import (
    SWITCH_ENDPOINTS,
    TOKEN_DELETE_PATH,
    TOKEN_GENERATE_PATH,
    TOKEN_INQUIRY_PATH,
    TOKEN_PAYMENT_PATH,
)
from garantipay.switch_hash import compute_switch_request_hash
from garantipay.switch_models import (
    SwitchAcquirer,
    SwitchCard,
    SwitchCardHolder,
    SwitchHeader,
    SwitchPaymentResponse,
    SwitchResponse,
    SwitchTokenResponse,
)

logger = logging.getLogger("garantipay.switch")


class SwitchClient:
    """High-level client for Garanti BBVA Switch (Card Storage) API.

    Handles authentication, hash computation, and provides convenient
    methods for token management and token-based payments.

    To obtain your Switch credentials for production:

    1. Complete your integration in the test environment.
    2. Send your Garanti Sanalpos terminal number to
       ETicaretDestek@garantibbva.com.tr to receive your Switch ID.
    3. Create your Switch Password in the merchant management screens
       under the "Password Update" menu.

    Args:
        swt_id: Switch ID assigned to your merchant account.
            Test value: ``"CC82C381E078482AB328943FCCB7100C"``.
        swt_password: Switch password for hash computation.
            Test value: ``"123asdASD@"``.
        user_id: User identifier included in request headers for
            audit/tracking. Can be any value, but should be unique
            to your system.
        mode: ``"TEST"`` for sandbox or ``"PROD"`` for production.
            Defaults to ``"TEST"``.
        timeout: HTTP request timeout in seconds. Defaults to ``30``.

    Example::

        from garantipay import SwitchClient

        client = SwitchClient(
            swt_id="CC82C381E078482AB328943FCCB7100C",
            swt_password="123asdASD@",
            user_id="my_system",
            mode="TEST",
        )
    """

    def __init__(
        self,
        swt_id: str,
        swt_password: str,
        user_id: str,
        mode: str = "TEST",
        timeout: int = 30,
    ) -> None:
        self._swt_id = swt_id
        self._swt_password = swt_password
        self._user_id = user_id
        self._mode = mode
        self._timeout = timeout
        self._base_url = SWITCH_ENDPOINTS.get(mode, SWITCH_ENDPOINTS["TEST"])

    def generate_token(
        self,
        card_number: str,
        expire_month: str,
        expire_year: str,
        cvv: Optional[str] = None,
        holder_name: Optional[str] = None,
        additional_data: Optional[str] = None,
        card_holder: Optional[SwitchCardHolder] = None,
    ) -> SwitchTokenResponse:
        """Generate a token by storing card data securely on Garanti servers.

        The card is validated during this process. For non-Garanti cards,
        a 0.11 TRY test charge is made and automatically reversed (not
        visible on the cardholder's statement).

        Save the returned ``token`` in your database linked to the customer.
        Use it later with :meth:`sale_with_token` for recurring payments.

        Args:
            card_number: Full card number (PAN).
            expire_month: Expiration month in ``"MM"`` format (e.g. ``"02"``).
            expire_year: Expiration year in ``"YY"`` format (e.g. ``"25"``).
            cvv: Card CVV code (3 digits). Optional but recommended.
            holder_name: Name as printed on the card. Optional.
            additional_data: Extra data to associate with the token (max 120 chars).
                If not sent, the same card always produces the same token.
                If sent, a unique token is generated per ``additional_data`` value.
                Useful for linking tokens to policy numbers, subscriptions, etc.
            card_holder: Optional cardholder information (email, phone, etc.).

        Returns:
            A :class:`~garantipay.switch_models.SwitchTokenResponse` containing
            the generated token ID, masked card number, and bank information.

        Raises:
            GarantiPayValidationError: If required card fields are missing.
            GarantiPayConnectionError: If the HTTP request fails.

        Example::

            response = client.generate_token(
                card_number="5549601634451019",
                expire_month="02",
                expire_year="25",
            )
            if response.is_successful:
                token = response.card.token
                print(f"Token generated: {token}")
                print(f"Card: {response.card.masked_number}")
        """
        if not card_number:
            raise GarantiPayValidationError("card_number is required for token generation.")
        if not expire_month:
            raise GarantiPayValidationError("expire_month is required for token generation.")
        if not expire_year:
            raise GarantiPayValidationError("expire_year is required for token generation.")
        header = self._build_header()
        card = SwitchCard(
            number=card_number,
            expire_month=expire_month,
            expire_year=expire_year,
            cvv=cvv,
            holder_name=holder_name,
        )
        payload: Dict[str, Any] = {
            "header": header.to_dict(),
            "registrationType": "F",
            "card": card.to_dict(),
        }
        if additional_data is not None:
            payload["additionalData"] = additional_data
        if card_holder is not None:
            payload["cardHolder"] = card_holder.to_dict()
        data = self._post(TOKEN_GENERATE_PATH, payload)
        return SwitchTokenResponse.from_dict(data)

    def inquiry_token(self, original_request_id: str) -> SwitchTokenResponse:
        """Query a previously generated token.

        Args:
            original_request_id: The ``request_id`` from the original
                :meth:`generate_token` call. This is available on the
                response as ``response.header.request_id``.

        Returns:
            A :class:`~garantipay.switch_models.SwitchTokenResponse` with
            the token details.

        Raises:
            GarantiPayValidationError: If ``original_request_id`` is empty.
            GarantiPayConnectionError: If the HTTP request fails.

        Example::

            response = client.inquiry_token(
                original_request_id="36d5856c34e34799927571fe9cc18fca"
            )
            if response.is_successful:
                print(f"Token: {response.card.token}")
                print(f"Bank: {response.card.bank_name}")
        """
        if not original_request_id:
            raise GarantiPayValidationError(
                "original_request_id is required for token inquiry."
            )
        header = self._build_header()
        payload: Dict[str, Any] = {
            "header": header.to_dict(),
            "originalRequestId": original_request_id,
        }
        data = self._post(TOKEN_INQUIRY_PATH, payload)
        return SwitchTokenResponse.from_dict(data)

    def delete_token(self, token: str) -> SwitchResponse:
        """Delete a stored token from Garanti servers.

        Once deleted, the token can no longer be used for payments.
        The card data associated with it is permanently removed.

        Args:
            token: The token ID to delete (32-character string).

        Returns:
            A :class:`~garantipay.switch_models.SwitchResponse` indicating
            success or failure.

        Raises:
            GarantiPayValidationError: If ``token`` is empty.
            GarantiPayConnectionError: If the HTTP request fails.

        Example::

            response = client.delete_token("281A91A144B344979C3C78F7439FE857")
            if response.is_successful:
                print("Token deleted successfully.")
        """
        if not token:
            raise GarantiPayValidationError("token is required for token deletion.")
        header = self._build_header()
        payload: Dict[str, Any] = {
            "header": header.to_dict(),
            "card": {"token": token},
        }
        data = self._post(TOKEN_DELETE_PATH, payload)
        return SwitchResponse.from_dict(data)

    def sale_with_token(
        self,
        token: str,
        amount: str,
        customer_ip: str,
        currency_code: str = "949",
        order_id: Optional[str] = None,
        installment_count: Optional[str] = None,
        additional_data: Optional[str] = None,
        moto_ind: Optional[str] = None,
        generate_order_id: str = "Y",
        bank_id: Optional[str] = None,
        card_holder: Optional[SwitchCardHolder] = None,
    ) -> SwitchPaymentResponse:
        """Process a payment using a stored token (for recurring/subscription billing).

        This is the primary method for charging a customer whose card
        has been previously tokenized via :meth:`generate_token`.

        .. important::
            The ``amount`` format is different from the VPServlet API.
            Here, use decimal notation with a period separator:
            ``"10.50"`` means 10.50 TRY (not ``"1050"``).

        Args:
            token: Token ID from a previous :meth:`generate_token` call.
            amount: Transaction amount with period-separated decimals.
                Example: ``"10.50"`` for 10.50 TRY.
            customer_ip: Customer's IP address in IPv4 format. **Required.**
            currency_code: ISO 4217 numeric currency code. Defaults to
                ``"949"`` (TRY). Use :data:`garantipay.CURRENCIES` for
                lookup: ``CURRENCIES["USD"]`` returns ``"840"``.
            order_id: Optional order ID. If not provided and
                ``generate_order_id`` is ``"Y"``, the Switch system
                generates one automatically.
            installment_count: Number of installments. ``"0"`` or ``None``
                for single payment. ``"3"`` for 3 installments.
            additional_data: Must match the ``additional_data`` used during
                token generation, if one was provided.
            moto_ind: ``"Y"`` for Mail Order/Telephone Order transactions.
            generate_order_id: ``"Y"`` to let Switch generate an order ID
                matching the target bank's format (recommended).
                ``"N"`` to use your own ``order_id`` as-is.
            bank_id: Target bank ID for routing. If ``None``, Switch
                routes automatically based on the card's issuing bank.
            card_holder: Optional cardholder details.

        Returns:
            A :class:`~garantipay.switch_models.SwitchPaymentResponse`
            with the transaction result, reference numbers, and bank response.

        Raises:
            GarantiPayValidationError: If required fields are missing.
            GarantiPayConnectionError: If the HTTP request fails.

        Example::

            response = client.sale_with_token(
                token="281A91A144B344979C3C78F7439FE857",
                amount="29.90",
                customer_ip="192.168.1.1",
                currency_code="949",
            )
            if response.is_successful:
                print(f"Payment approved!")
                print(f"  Ref: {response.acquirer_response.ret_ref_num}")
                print(f"  Auth: {response.acquirer_response.auth_code}")
            else:
                print(f"Payment failed: {response.error_message}")
        """
        if not token:
            raise GarantiPayValidationError("token is required for token-based payment.")
        if not amount:
            raise GarantiPayValidationError("amount is required.")
        if not customer_ip:
            raise GarantiPayValidationError("customer_ip is required.")
        header = self._build_header()
        card = SwitchCard(token=token)
        holder = card_holder or SwitchCardHolder(ip=customer_ip)
        if holder.ip is None:
            holder.ip = customer_ip
        acquirer = SwitchAcquirer(bank_id=bank_id)
        payload: Dict[str, Any] = {
            "header": header.to_dict(),
            "card": card.to_dict(),
            "cardHolder": holder.to_dict(),
            "acquirer": acquirer.to_dict(),
            "txnAmount": amount,
            "currencyNumber": currency_code,
            "generateOrderId": generate_order_id,
        }
        if order_id is not None:
            payload["orderId"] = order_id
        if installment_count is not None:
            payload["installmentCount"] = installment_count
        else:
            payload["installmentCount"] = "0"
        if additional_data is not None:
            payload["additionalData"] = additional_data
        if moto_ind is not None:
            payload["motoInd"] = moto_ind
        data = self._post(TOKEN_PAYMENT_PATH, payload)
        return SwitchPaymentResponse.from_dict(data)

    def _build_header(self) -> SwitchHeader:
        """Build an authenticated request header with auto-generated ID and timestamp."""
        request_id = uuid.uuid4().hex
        timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
        hashed_data = compute_switch_request_hash(
            request_id=request_id,
            swt_id=self._swt_id,
            user_id=self._user_id,
            timestamp=timestamp,
            swt_password=self._swt_password,
        )
        return SwitchHeader(
            request_id=request_id,
            swt_id=self._swt_id,
            user_id=self._user_id,
            timestamp=timestamp,
            hashed_data=hashed_data,
        )

    def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
        """Send a POST request to the Switch API and return the JSON response."""
        url = f"{self._base_url}{path}"
        logger.debug("POST %s", url)
        logger.debug("Request payload: %s", payload)
        try:
            response = http_requests.post(
                url,
                json=payload,
                headers={"Content-Type": "application/json; charset=utf-8"},
                timeout=self._timeout,
            )
            logger.debug("Response status: %s", response.status_code)
            logger.debug("Response body: %s", response.text)
            return response.json()
        except http_requests.RequestException as exc:
            raise GarantiPayConnectionError(
                f"Failed to connect to Garanti BBVA Switch API at {url}: {exc}"
            ) from exc
        except ValueError as exc:
            raise GarantiPayConnectionError(
                f"Invalid JSON response from Switch API at {url}: {exc}"
            ) from exc
