"""HTTP helper that works under PySide6's QtAsyncio event loop.

QtAsyncio implements none of asyncio's networking primitives — getaddrinfo(),
sock_connect(), add_reader() etc. all raise NotImplementedError — so
httpx.AsyncClient can never complete a request inside the app (the failure
surfaced as "Activation failed unexpectedly" with zero traffic reaching the
server). The one concurrency primitive QtAsyncio does implement is
run_in_executor, so every backend call runs a synchronous httpx request in a
worker thread and awaits the resulting future.
"""
from __future__ import annotations

import asyncio
import time
from typing import Any

import httpx
from loguru import logger

# Transient transport failures worth retrying (connect refused, DNS hiccup,
# read timeout on a slow link). A returned HTTP response — even 5xx — is NOT
# retried here: it is a real answer the caller must interpret (a 403 "invalid
# key" must never be retried into a spurious success).
_RETRYABLE = (
    httpx.ConnectError,
    httpx.ConnectTimeout,
    httpx.ReadTimeout,
    httpx.WriteTimeout,
    httpx.PoolTimeout,
    httpx.RemoteProtocolError,
)
_RETRY_BACKOFF = (0.6, 1.8)  # seconds before attempt 2 and 3 (3 attempts total)


async def arequest(
    method: str,
    url: str,
    *,
    timeout: httpx.Timeout,
    follow_redirects: bool = False,
    retries: int = len(_RETRY_BACKOFF),
    verify: Any = True,
    **kwargs: Any,
) -> httpx.Response:
    """Perform a blocking httpx request in a worker thread and await it.

    Retries transient transport errors with backoff so a single dropped packet
    or slow DNS lookup doesn't turn into a failed activation on a flaky link.
    Raises httpx.HTTPError on network failures (after exhausting retries),
    exactly like the async client, so existing except-clauses keep working.

    `verify` is passed to httpx (True = normal CA validation; an ssl.SSLContext
    for pinned self-signed servers; False only as a deliberate insecure
    fallback). Building the SSLContext stays with the caller so the pinning
    policy lives in one place (tls_trust.py).
    """
    loop = asyncio.get_running_loop()

    def _do() -> httpx.Response:
        # Whole retry loop runs in the worker thread; time.sleep here blocks the
        # worker, never the Qt/asyncio event loop.
        last_exc: Exception | None = None
        for attempt in range(retries + 1):
            try:
                with httpx.Client(
                    timeout=timeout, follow_redirects=follow_redirects,
                    verify=verify,
                ) as client:
                    return client.request(method, url, **kwargs)
            except _RETRYABLE as e:
                last_exc = e
                if attempt < retries:
                    delay = _RETRY_BACKOFF[min(attempt, len(_RETRY_BACKOFF) - 1)]
                    logger.info(
                        "HTTP {} {} transient error ({}); retry {}/{} in {}s",
                        method, url, e.__class__.__name__, attempt + 1, retries, delay,
                    )
                    time.sleep(delay)
                    continue
                raise
        assert last_exc is not None  # unreachable; loop either returns or raises
        raise last_exc

    return await loop.run_in_executor(None, _do)


async def run_blocking(func: Any, *args: Any) -> Any:
    """Run a blocking callable in the default executor (QtAsyncio-safe)."""
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(None, func, *args)
