"""HTTP client for the Garanti BBVA Virtual POS (GVP) API.

This module provides two client classes:

- :class:`API` -- Low-level client that sends a pre-built
  :class:`~garantipay.models.Request` to the API. Gives you full control
  over every field. This is the original API preserved for backward
  compatibility and advanced use cases.

- :class:`GarantiClient` -- High-level client that handles hash computation,
  request assembly, and common transaction types (sale, refund, void, etc.)
  through simple method calls. **Recommended for most users.**

Example (high-level)::

    from garantipay import GarantiClient, CURRENCIES

    client = GarantiClient(
        terminal_id="12345678",
        merchant_id="1234567",
        prov_user_id="PROVAUT",
        user_id="1234567",
        password="123qweASD*",
        mode="TEST",
    )

    response = client.sale(
        card_number="1234567890123456",
        card_expire_date="0326",
        card_cvv2="123",
        amount="100",
        currency="TRY",
        customer_ip="1.2.3.4",
    )

    if response.is_successful:
        print(f"Approved! Ref: {response.transaction.retref_num}")
    else:
        print(f"Declined: {response.error_message}")
"""

import logging
from typing import Optional

import requests as http_requests

from garantipay.constants import CURRENCIES, ENDPOINTS
from garantipay.exceptions import (
    GarantiPayConnectionError,
    GarantiPayValidationError,
)
from garantipay.hash import compute_hash_data
from garantipay.models import (
    Card,
    Customer,
    Order,
    Request,
    Response,
    Secure3D,
    Terminal,
    Transaction,
)
from garantipay.xml_builder import build_request_xml
from garantipay.xml_parser import parse_response_xml

logger = logging.getLogger("garantipay")


class API:
    """Low-level API client for sending pre-built requests to the GVP endpoint.

    Use this when you need full control over every field in the XML request.
    For most use cases, prefer :class:`GarantiClient` instead.

    Args:
        timeout: HTTP request timeout in seconds. Defaults to ``30``.

    Example::

        from garantipay import API, Request, Terminal, Customer, Transaction
        from garantipay.hash import compute_hash_data

        api = API(timeout=60)
        request = Request(mode="TEST", version="v1.0")
        request.terminal = Terminal(
            id="12345678",
            merchant_id="1234567",
            prov_user_id="PROVAUT",
            user_id="1234567",
            hash_data=compute_hash_data(
                password="secret",
                terminal_id="12345678",
                amount="100",
            ),
        )
        request.customer = Customer(ip_address="1.2.3.4")
        request.transaction = Transaction(type="sales", amount="100")
        response = api.transaction(request)
    """

    def __init__(self, timeout: int = 30) -> None:
        self._timeout = timeout

    def transaction(self, request: Request) -> Response:
        """Send a transaction request to the Garanti BBVA API.

        Args:
            request: A fully populated :class:`~garantipay.models.Request` object.
                The ``terminal.hash_data`` field must be pre-computed.

        Returns:
            A :class:`~garantipay.models.Response` object with the parsed result.

        Raises:
            GarantiPayConnectionError: If the HTTP request fails due to
                network issues, timeouts, or DNS failures.
        """
        post_data = build_request_xml(request)
        endpoint = ENDPOINTS.get(request.mode or "TEST", ENDPOINTS["TEST"])
        logger.debug("Sending request to %s", endpoint)
        logger.debug("Request XML: %s", post_data)
        try:
            http_response = http_requests.post(
                endpoint,
                data=post_data,
                headers={"Content-Type": "text/xml; charset=utf-8"},
                timeout=self._timeout,
            )
            logger.debug("Response status: %s", http_response.status_code)
            logger.debug("Response body: %s", http_response.text)
            return parse_response_xml(http_response.text)
        except http_requests.RequestException as exc:
            raise GarantiPayConnectionError(
                f"Failed to connect to Garanti BBVA API at {endpoint}: {exc}"
            ) from exc


class GarantiClient:
    """High-level client for Garanti BBVA Virtual POS transactions.

    Handles hash computation, request assembly, and provides convenient
    methods for common transaction types. This is the **recommended** way
    to interact with the API.

    Args:
        terminal_id: Terminal ID (Terminal No) assigned by the bank.
        merchant_id: Merchant ID (Isyeri No) assigned by the bank.
        prov_user_id: Provisioning user ID. ``"PROVAUT"`` for sales,
            ``"PROVRFN"`` for refunds. Can be overridden per-method.
        user_id: API user ID for authentication.
        password: Provisioning user password.
        mode: ``"TEST"`` for sandbox or ``"PROD"`` for production.
            Defaults to ``"TEST"``.
        version: API version string. Defaults to ``"v1.0"``.
        timeout: HTTP request timeout in seconds. Defaults to ``30``.

    Example::

        from garantipay import GarantiClient

        client = GarantiClient(
            terminal_id="12345678",
            merchant_id="1234567",
            prov_user_id="PROVAUT",
            user_id="99999999999",
            password="123qweASD*",
            mode="TEST",
        )
    """

    def __init__(
        self,
        terminal_id: str,
        merchant_id: str,
        prov_user_id: str,
        user_id: str,
        password: str,
        mode: str = "TEST",
        version: str = "v1.0",
        timeout: int = 30,
    ) -> None:
        self._terminal_id = terminal_id
        self._merchant_id = merchant_id
        self._prov_user_id = prov_user_id
        self._user_id = user_id
        self._password = password
        self._mode = mode
        self._version = version
        self._api = API(timeout=timeout)

    def sale(
        self,
        card_number: str,
        card_expire_date: str,
        card_cvv2: str,
        amount: str,
        customer_ip: str,
        currency: str = "TRY",
        order_id: Optional[str] = None,
        installment_count: Optional[str] = None,
        moto_ind: str = "N",
        description: Optional[str] = None,
        customer_email: Optional[str] = None,
        secure_3d: Optional[Secure3D] = None,
        cardholder_present_code: Optional[str] = None,
    ) -> Response:
        """Execute a sale (payment) transaction.

        Charges the given card for the specified amount.

        Args:
            card_number: Full card number (PAN).
            card_expire_date: Expiration date in ``MMYY`` format.
            card_cvv2: Card security code (3 or 4 digits).
            amount: Amount in minor units (last 2 digits are fractional).
                ``"100"`` means 1.00 in the specified currency.
            customer_ip: Customer's IP address (**required** by the gateway).
            currency: Currency code (e.g. ``"TRY"``, ``"USD"``). Defaults to ``"TRY"``.
            order_id: Optional order ID. If ``None``, the gateway auto-generates one.
            installment_count: Number of installments. ``None`` or ``""`` for
                single payment.
            moto_ind: Mail Order / Telephone Order indicator. ``"N"`` for
                e-commerce (default), ``"Y"`` for MOTO.
            description: Optional transaction description.
            customer_email: Optional customer email address.
            secure_3d: 3D Secure authentication data, if applicable.
            cardholder_present_code: Set to ``"13"`` for 3D Secure transactions.

        Returns:
            A :class:`~garantipay.models.Response` with the transaction result.

        Raises:
            GarantiPayValidationError: If required fields are missing.
            GarantiPayConnectionError: If the HTTP request fails.
        """
        self._validate_card_fields(card_number, card_expire_date, card_cvv2)
        self._validate_required_fields(amount, customer_ip)
        currency_code = self._resolve_currency(currency)
        hash_data = compute_hash_data(
            password=self._password,
            terminal_id=self._terminal_id,
            order_id=order_id or "",
            card_number=card_number,
            amount=amount,
        )
        request = Request(
            mode=self._mode,
            version=self._version,
            terminal=Terminal(
                id=self._terminal_id,
                merchant_id=self._merchant_id,
                prov_user_id=self._prov_user_id,
                user_id=self._user_id,
                hash_data=hash_data,
            ),
            customer=Customer(
                ip_address=customer_ip,
                email_address=customer_email,
            ),
            card=Card(
                number=card_number,
                expire_date=card_expire_date,
                cvv2=card_cvv2,
            ),
            order=Order(order_id=order_id or ""),
            transaction=Transaction(
                type="sales",
                amount=amount,
                currency_code=currency_code,
                installment_cnt=installment_count or "",
                moto_ind=moto_ind,
                description=description,
                secure_3d=secure_3d,
                cardholder_present_code=cardholder_present_code,
            ),
        )
        return self._api.transaction(request)

    def refund(
        self,
        amount: str,
        customer_ip: str,
        order_id: str,
        currency: str = "TRY",
        prov_user_id: Optional[str] = None,
        description: Optional[str] = None,
        customer_email: Optional[str] = None,
    ) -> Response:
        """Execute a refund for a previously completed transaction.

        Args:
            amount: Refund amount in minor units. Must not exceed the
                original transaction amount.
            customer_ip: Customer's IP address (**required** by the gateway).
            order_id: The original order ID of the transaction to refund.
                **Required** for refund operations.
            currency: Currency code. Defaults to ``"TRY"``.
            prov_user_id: Override the provisioning user ID for this
                refund. Typically ``"PROVRFN"``. If ``None``, uses the
                client's default ``prov_user_id``.
            description: Optional refund description/reason.
            customer_email: Optional customer email address.

        Returns:
            A :class:`~garantipay.models.Response` with the refund result.

        Raises:
            GarantiPayValidationError: If ``order_id`` is empty.
            GarantiPayConnectionError: If the HTTP request fails.
        """
        if not order_id:
            raise GarantiPayValidationError("order_id is required for refund transactions.")
        self._validate_required_fields(amount, customer_ip)
        currency_code = self._resolve_currency(currency)
        effective_prov_user_id = prov_user_id or self._prov_user_id
        hash_data = compute_hash_data(
            password=self._password,
            terminal_id=self._terminal_id,
            order_id=order_id,
            amount=amount,
        )
        request = Request(
            mode=self._mode,
            version=self._version,
            terminal=Terminal(
                id=self._terminal_id,
                merchant_id=self._merchant_id,
                prov_user_id=effective_prov_user_id,
                user_id=effective_prov_user_id,
                hash_data=hash_data,
            ),
            customer=Customer(
                ip_address=customer_ip,
                email_address=customer_email,
            ),
            order=Order(order_id=order_id),
            transaction=Transaction(
                type="refund",
                amount=amount,
                currency_code=currency_code,
                description=description,
            ),
        )
        return self._api.transaction(request)

    def void(
        self,
        amount: str,
        customer_ip: str,
        order_id: str,
        retref_num: str,
        auth_code: str,
        currency: str = "TRY",
        description: Optional[str] = None,
        customer_email: Optional[str] = None,
    ) -> Response:
        """Cancel (void) a transaction before settlement.

        A void must be performed on the same day, before the daily
        settlement batch is processed.

        Args:
            amount: The original transaction amount in minor units.
            customer_ip: Customer's IP address.
            order_id: The original order ID.
            retref_num: Retrieval reference number from the original
                transaction response.
            auth_code: Authorization code from the original transaction.
            currency: Currency code. Defaults to ``"TRY"``.
            description: Optional void reason/description.
            customer_email: Optional customer email address.

        Returns:
            A :class:`~garantipay.models.Response` with the void result.

        Raises:
            GarantiPayValidationError: If required fields are missing.
            GarantiPayConnectionError: If the HTTP request fails.
        """
        if not order_id:
            raise GarantiPayValidationError("order_id is required for void transactions.")
        if not retref_num:
            raise GarantiPayValidationError("retref_num is required for void transactions.")
        self._validate_required_fields(amount, customer_ip)
        currency_code = self._resolve_currency(currency)
        hash_data = compute_hash_data(
            password=self._password,
            terminal_id=self._terminal_id,
            order_id=order_id,
            amount=amount,
        )
        request = Request(
            mode=self._mode,
            version=self._version,
            terminal=Terminal(
                id=self._terminal_id,
                merchant_id=self._merchant_id,
                prov_user_id=self._prov_user_id,
                user_id=self._user_id,
                hash_data=hash_data,
            ),
            customer=Customer(
                ip_address=customer_ip,
                email_address=customer_email,
            ),
            order=Order(order_id=order_id),
            transaction=Transaction(
                type="void",
                amount=amount,
                currency_code=currency_code,
                description=description,
            ),
        )
        return self._api.transaction(request)

    def execute(self, request: Request) -> Response:
        """Send an arbitrary pre-built request to the API.

        This is a passthrough to :meth:`API.transaction` for cases
        where you need full control over the request structure.

        Args:
            request: A fully populated request object.

        Returns:
            A :class:`~garantipay.models.Response` with the result.
        """
        return self._api.transaction(request)

    def _resolve_currency(self, currency: str) -> str:
        """Convert a currency abbreviation to its ISO 4217 numeric code."""
        code = CURRENCIES.get(currency.upper())
        if code is None:
            raise GarantiPayValidationError(
                f"Unknown currency '{currency}'. "
                f"Supported currencies: {', '.join(sorted(CURRENCIES.keys()))}"
            )
        return code

    @staticmethod
    def _validate_card_fields(card_number: str, expire_date: str, cvv2: str) -> None:
        """Validate that card fields are not empty."""
        if not card_number:
            raise GarantiPayValidationError("card_number is required.")
        if not expire_date:
            raise GarantiPayValidationError("card_expire_date is required.")
        if not cvv2:
            raise GarantiPayValidationError("card_cvv2 is required.")

    @staticmethod
    def _validate_required_fields(amount: str, customer_ip: str) -> None:
        """Validate that amount and customer IP are not empty."""
        if not amount:
            raise GarantiPayValidationError("amount is required.")
        if not customer_ip:
            raise GarantiPayValidationError("customer_ip is required.")
