"""Small, Django-independent primitives for the Mollie provider boundary.

This module deliberately has no configuration lookup at import time. Projects
must pass a key at runtime and Oscar-specific behaviour stays in the adapter.
"""

from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
import re
from typing import Any, Mapping


class MollieProviderError(RuntimeError):
    """Base error raised at the provider boundary."""


class MollieConfigurationError(MollieProviderError):
    """Raised for absent or invalid runtime provider configuration."""


class MollieResponseError(MollieProviderError):
    """Raised when Mollie returns an unusable response."""


_CURRENCY_RE = re.compile(r"^[A-Z]{3}$")
_TERMINAL_STATUSES = frozenset({"paid", "failed", "canceled", "expired"})
_KNOWN_STATUSES = _TERMINAL_STATUSES | frozenset({"open", "pending", "authorized"})


def validate_api_key(api_key: str) -> str:
    """Validate the shape of an API key without ever logging its value."""
    value = str(api_key or "").strip()
    if not value or value.upper() == "CHANGE_ME":
        raise MollieConfigurationError(
            "A Mollie API key must be configured at runtime."
        )
    if not value.startswith(("test_", "live_")):
        raise MollieConfigurationError(
            "Mollie API keys must use the test_ or live_ prefix."
        )
    return value


def _format_amount(amount: Any, currency: str, *, allow_zero: bool) -> dict[str, str]:
    """Return an exact Mollie monetary object with a validated ISO currency."""
    code = str(currency or "").upper()
    if not _CURRENCY_RE.fullmatch(code):
        raise MollieConfigurationError(
            "Mollie currency must be an ISO 4217 three-letter code."
        )
    try:
        value = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    except (InvalidOperation, ValueError) as exc:
        raise MollieConfigurationError(
            "Mollie amount must be a decimal value."
        ) from exc
    if value < 0 or (value == 0 and not allow_zero):
        raise MollieConfigurationError("Mollie amount must be greater than zero.")
    return {"currency": code, "value": format(value, ".2f")}


def format_amount(amount: Any, currency: str) -> dict[str, str]:
    """Return a positive two-decimal amount for a payment or refund request."""
    return _format_amount(amount, currency, allow_zero=False)


def format_component_amount(amount: Any, currency: str) -> dict[str, str]:
    """Return a non-negative amount for an order-line tax component."""
    return _format_amount(amount, currency, allow_zero=True)


def payment_status(payment: Mapping[str, Any] | Any) -> str:
    """Extract and validate a Mollie payment status from an SDK object."""
    status = payment.get("status") if hasattr(payment, "get") else None
    status = str(status or "").lower()
    if status not in _KNOWN_STATUSES:
        raise MollieResponseError("Mollie returned an unknown payment status.")
    return status


def is_terminal_status(status: str) -> bool:
    return status in _TERMINAL_STATUSES


def checkout_url(payment: Mapping[str, Any] | Any) -> str:
    """Read the documented checkout link and accept the SDK legacy property."""
    links = payment.get("_links") if hasattr(payment, "get") else None
    # Older SDK response fixtures used ``links``. Keep this read-only
    # compatibility fallback while emitting only current API payloads.
    if not links and hasattr(payment, "get"):
        links = payment.get("links")
    checkout = links.get("checkout") if isinstance(links, Mapping) else None
    url = checkout.get("href") if isinstance(checkout, Mapping) else None
    url = url or getattr(payment, "checkout_url", None)
    if not isinstance(url, str) or not url.startswith(("https://", "http://")):
        raise MollieResponseError(
            "Mollie response did not include a valid checkout URL."
        )
    return url


@dataclass(frozen=True)
class MollieClientSettings:
    api_key: str
    timeout: tuple[int, int] = (2, 10)
    retry: int = 3

    def validated(self) -> "MollieClientSettings":
        return MollieClientSettings(
            api_key=validate_api_key(self.api_key),
            timeout=self.timeout,
            retry=self.retry,
        )


def create_client(settings: MollieClientSettings):
    """Create the official SDK client lazily after validated runtime setup."""
    from mollie.api.client import Client

    validated = settings.validated()
    client = Client(timeout=validated.timeout, retry=validated.retry)
    client.set_api_key(validated.api_key)
    return client
