"""Infomaniak Manager-API adapter — the real HTTP client.

Implements :class:`~mgf.cloud.manager.base.ManagerApi` against
``https://api.infomaniak.com`` with a Bearer token. Uses only the standard
library (``urllib``) behind a small :class:`Transport` seam, so the core stays
light (no HTTP dependency, DP-02) and unit tests inject a fake transport — no
network (TS-02). Every request carries a finite timeout (SC-07) and speaks HTTPS
only (SC-06); transient 429/5xx and transport errors are retried with capped
backoff (EH-11). The Bearer token comes from a ``SecretStr`` and is never logged.
"""

from __future__ import annotations

import json
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol
from urllib.parse import urlsplit

from mgf.common.observability import get_logger

from mgf.cloud.exceptions import ManagerApiError, ResourceNotFoundError
from mgf.cloud.manager.models import DnsRecord, HostingSite, Product, WebHosting

if TYPE_CHECKING:
    from mgf.cloud.manager.config import ManagerCredentials

__all__ = ["HttpResponse", "InfomaniakManager", "Transport", "UrllibTransport"]

LOG = get_logger(__name__)

_RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504})
_NOT_FOUND_CODES = frozenset({"not_found", "object_not_found"})
_MAX_BACKOFF = 4.0


@dataclass(frozen=True)
class HttpResponse:
    """A minimal HTTP response — the only thing a :class:`Transport` returns."""

    status: int
    body: bytes


class Transport(Protocol):
    """The HTTP seam. The real impl uses ``urllib``; tests inject a fake."""

    def get(self, url: str, headers: dict[str, str], timeout: float) -> HttpResponse:
        """GET ``url`` and return the status + raw body (never raises for 4xx/5xx)."""
        ...


class UrllibTransport:
    """Standard-library HTTPS GET transport (no third-party dependency)."""

    def get(self, url: str, headers: dict[str, str], timeout: float) -> HttpResponse:
        if urlsplit(url).scheme != "https":
            raise ManagerApiError(f"refusing to open non-https URL: {url!r}")
        request = urllib.request.Request(url, headers=headers, method="GET")  # noqa: S310 - scheme asserted https above
        try:
            with urllib.request.urlopen(request, timeout=timeout) as response:  # noqa: S310
                return HttpResponse(status=int(response.status), body=response.read())
        except urllib.error.HTTPError as exc:
            # A 4xx/5xx still carries the API's JSON error body — surface it, don't raise.
            return HttpResponse(status=int(exc.code), body=exc.read())


def _as_list(data: Any) -> list[dict[str, Any]]:
    if not isinstance(data, list):
        raise ManagerApiError(f"expected a JSON list, got {type(data).__name__}")
    return [item for item in data if isinstance(item, dict)]


def _as_dict(data: Any) -> dict[str, Any]:
    if not isinstance(data, dict):
        raise ManagerApiError(f"expected a JSON object, got {type(data).__name__}")
    return data


def _int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _opt_int(value: Any) -> int | None:
    return None if value is None else _int(value)


def _product(item: dict[str, Any]) -> Product:
    return Product(
        id=_int(item.get("id")),
        service_name=str(item.get("service_name", "")),
        customer_name=str(item.get("customer_name", "")),
        account_id=_opt_int(item.get("account_id")),
    )


def _web_hosting(item: dict[str, Any]) -> WebHosting:
    return WebHosting(
        id=_int(item.get("web_hosting_id", item.get("id"))),
        account_id=_int(item.get("account_id")),
        customer_name=str(item.get("customer_name", "")),
        version=_int(item.get("version")),
        is_free=bool(item.get("is_free", False)),
        is_zero_price=bool(item.get("is_zero_price", False)),
    )


def _site(item: dict[str, Any]) -> HostingSite:
    return HostingSite(
        id=_int(item.get("id")),
        hosting_id=_int(item.get("parent_id")),
        main_fqdn=str(item.get("main_fqdn", "")),
        directory=str(item.get("directory", "/")),
        hosting_type=str(item.get("hosting_type", "")),
        ssl_status=str(item.get("ssl_status", "")),
        can_install_applications=bool(item.get("can_install_applications", False)),
    )


def _dns(item: dict[str, Any]) -> DnsRecord:
    return DnsRecord(
        id=str(item.get("id", "")),
        source=str(item.get("source", "")),
        type=str(item.get("type", "")),
        ttl=_int(item.get("ttl")),
        target=str(item.get("target", item.get("target_idn", ""))),
    )


class InfomaniakManager:
    """Read-only Infomaniak Manager-API client (a :class:`ManagerApi`)."""

    name = "infomaniak"

    def __init__(
        self,
        credentials: ManagerCredentials,
        *,
        transport: Transport | None = None,
        timeout: float = 15.0,
        retries: int = 2,
    ) -> None:
        self._creds = credentials
        self._transport: Transport = transport if transport is not None else UrllibTransport()
        self._timeout = timeout
        self._retries = max(0, retries)

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self._creds.token.get_secret_value()}",
            "Accept": "application/json",
        }

    def _get(self, path: str) -> Any:
        """GET ``path`` and return the response's ``data`` payload, with retries.

        Raises :class:`ResourceNotFoundError` on 404 and :class:`ManagerApiError`
        on any other API or transport failure.
        """
        url = f"{self._creds.base_url}/{path.lstrip('/')}"
        error = ManagerApiError(f"Manager request to {path} did not complete")
        for attempt in range(self._retries + 1):
            try:
                response = self._transport.get(url, self._headers(), self._timeout)
            except (urllib.error.URLError, TimeoutError, OSError) as exc:
                error = ManagerApiError(f"Manager request to {path} failed: {exc}")
            else:
                if response.status not in _RETRYABLE_STATUS:
                    return self._payload(path, response)
                error = ManagerApiError(
                    f"Manager returned {response.status} for {path}", status=response.status
                )
            if attempt < self._retries:
                LOG.warning("manager GET %s: retrying (attempt %d)", path, attempt + 1)
                time.sleep(min(0.5 * (2**attempt), _MAX_BACKOFF))
        raise error

    def _payload(self, path: str, response: HttpResponse) -> Any:
        try:
            body = json.loads(response.body.decode("utf-8"))
        except (ValueError, UnicodeDecodeError) as exc:
            raise ManagerApiError(
                f"Manager returned non-JSON for {path}", status=response.status
            ) from exc
        envelope = _as_dict(body)
        if envelope.get("result") == "success":
            return envelope.get("data")
        err = envelope.get("error")
        code = str(err.get("code", "")) if isinstance(err, dict) else ""
        message = str(err.get("description", "")) if isinstance(err, dict) else ""
        if response.status == 404 or code in _NOT_FOUND_CODES:
            raise ResourceNotFoundError(message or f"{path} not found")
        raise ManagerApiError(
            message or f"Manager error for {path}", status=response.status, code=code
        )

    # --- ManagerApi ----------------------------------------------------------
    def list_products(self) -> tuple[Product, ...]:
        return tuple(_product(item) for item in _as_list(self._get("1/products")))

    def list_web_hostings(self) -> tuple[WebHosting, ...]:
        return tuple(
            _web_hosting(item)
            for item in _as_list(self._get("1/products"))
            if item.get("service_name") == "hosting"
        )

    def get_web_hosting(self, hosting_id: int) -> WebHosting:
        return _web_hosting(_as_dict(self._get(f"1/web_hostings/{hosting_id}")))

    def find_web_hosting(self, fqdn: str) -> WebHosting:
        for item in _as_list(self._get("1/products")):
            if item.get("service_name") == "hosting" and item.get("customer_name") == fqdn:
                return self.get_web_hosting(_int(item.get("id")))
        raise ResourceNotFoundError(f"no web hosting for {fqdn!r}")

    def list_sites(self, hosting_id: int) -> tuple[HostingSite, ...]:
        return tuple(
            _site(item) for item in _as_list(self._get(f"1/web_hostings/{hosting_id}/sites"))
        )

    def get_site(self, hosting_id: int, site_id: int) -> HostingSite:
        return _site(_as_dict(self._get(f"1/web_hostings/{hosting_id}/sites/{site_id}")))

    def list_dns_records(self, fqdn: str) -> tuple[DnsRecord, ...]:
        return tuple(_dns(item) for item in _as_list(self._get(f"2/zones/{fqdn}/records")))
