import time

import httpcore
import httpx
from django.conf import settings
from supabase import Client, ClientOptions, create_client

_client = None

# Methods where retrying a failed request is always safe: none of these can
# duplicate a write. GET/HEAD have no side effects; every PATCH/DELETE in
# this app sets absolute values or deletes by id (never "increment by 1"),
# so repeating one is a no-op. POST (insert) is deliberately excluded —
# if the request body already reached the server before the response read
# failed, retrying it would risk creating a duplicate row (a duplicate
# order, message, etc. is worse than surfacing the original error).
_SAFE_RETRY_METHODS = {'GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'DELETE'}
# ReadTimeout/ConnectTimeout added alongside the original dead-connection
# errors below -- a slow/unresponsive Supabase round trip (seen repeatedly
# in practice, distinct from a silently-killed pooled connection) was
# previously NOT retried at all and surfaced as a raw 500 to the user. Safe
# for the same reason as the others: a timed-out GET/PATCH/DELETE/etc. never
# reached a state where retrying it could duplicate a write.
_RETRYABLE_EXCEPTIONS = (
    httpx.ReadError, httpx.ConnectError, httpx.RemoteProtocolError, httpcore.ReadError,
    httpx.ReadTimeout, httpx.ConnectTimeout,
)


class _RetryingTransport(httpx.HTTPTransport):
    """Pooled keep-alive connections here occasionally get silently killed
    by the network/remote between requests — the next request to reuse one
    fails with a ReadError (SSL bad-record-mac / connection reset) partway
    through reading the response. Once that happens, the same failure kept
    recurring on every subsequent request in the process until restart (see
    memory: supabase-httpx-pool-poisoning.md). Retrying on a fresh
    connection fixes it outright — for methods where a retry can never
    duplicate a write (see _SAFE_RETRY_METHODS above)."""

    def handle_request(self, request):
        method = request.method.upper()
        attempts = 6 if method in _SAFE_RETRY_METHODS else 1
        last_exc = None
        for attempt in range(attempts):
            try:
                return super().handle_request(request)
            except _RETRYABLE_EXCEPTIONS as exc:
                last_exc = exc
                if attempt < attempts - 1:
                    time.sleep(0.05 * (attempt + 1))
                continue
        raise last_exc


def get_client() -> Client:
    """Singleton, shared across every request thread. HTTP/2 is disabled on
    the underlying httpx client: with it on, concurrent requests issued from
    multiple threads against this one shared client intermittently crash
    inside httpcore's HTTP/2 stream handling (a real thread-safety bug hit
    while parallelizing the nav-badge lookups below). HTTP/1.1 instead pulls
    a separate pooled connection per concurrent request, which is safe.

    A short keepalive_expiry recycles pooled connections proactively before
    they go stale, and _RetryingTransport catches the (still possible) rare
    stale-connection failure and retries it on a fresh connection instead
    of surfacing a 500."""
    global _client
    if _client is None:
        transport = _RetryingTransport(
            http2=False,
            limits=httpx.Limits(max_connections=100, max_keepalive_connections=40, keepalive_expiry=5.0),
        )
        options = ClientOptions(httpx_client=httpx.Client(http2=False, transport=transport))
        _client = create_client(settings.SUPABASE_URL, settings.SUPABASE_ANON_KEY, options)
    return _client
