from __future__ import annotations

import asyncio
import logging
import time
from typing import Any, Dict, List, Optional, Tuple

import httpx

from .exceptions import (
    LogguardAuthError,
    LogguardConnectionError,
    LogguardConflictError,
    LogguardNotFoundError,
    LogguardQuotaError,
    LogguardValidationError,
)
from .models import AlertOut, IngestResult

log = logging.getLogger("logguard")

_RETRY_STATUSES = {500, 502, 503, 504}


def _safe_json(resp: httpx.Response) -> Tuple[Any, str]:
    """Returns (parsed_json_or_None, text_snippet). Never raises."""
    text = ""
    try:
        text = resp.text or ""
    except Exception:
        pass
    snippet = text[:400]
    try:
        j = resp.json()
        return j, snippet
    except Exception:
        return None, snippet


def _raise_for_status(resp: httpx.Response) -> Any:
    """
    Raises appropriate LogguardError for non-2xx responses.
    Returns parsed JSON body on success.
    """
    data, snippet = _safe_json(resp)
    d = data if isinstance(data, dict) else {}

    if resp.status_code == 401:
        raise LogguardAuthError("Invalid API key")

    if resp.status_code == 402:
        raise LogguardAuthError("Subscription expired — renew at logguard.io")

    if resp.status_code == 403:
        detail = d.get("detail", snippet)
        if detail == "subscription_expired":
            raise LogguardAuthError("Subscription expired — renew at logguard.io")
        raise LogguardAuthError(f"Access denied: {detail}")

    if resp.status_code == 404:
        detail = d.get("detail", snippet)
        raise LogguardNotFoundError(f"Not found: {detail}")

    if resp.status_code == 409:
        detail = d.get("detail", snippet)
        raise LogguardConflictError(f"Conflict: {detail}")

    if resp.status_code == 422:
        raise LogguardValidationError(f"Validation error: {snippet}")

    if resp.status_code == 429:
        detail = d.get("detail", {})
        if isinstance(detail, dict) and detail.get("err") == "usage_limit_exceeded":
            raise LogguardQuotaError(
                f"Monthly quota exceeded: {detail.get('used')}/{detail.get('limit')} "
                f"events on plan '{detail.get('plan')}'. Upgrade at logguard.io"
            )
        raise LogguardConnectionError(f"Rate limited: {snippet}")

    if resp.status_code >= 500:
        raise LogguardConnectionError(f"Server error ({resp.status_code}): {snippet}")

    return data


def parse_ingest_response(data: Any) -> IngestResult:
    d = data if isinstance(data, dict) else {}
    alerts = [AlertOut(**a) for a in (d.get("alerts") or [])]
    return IngestResult(
        ok=bool(d.get("ok", False)),
        inserted=int(d.get("accepted", d.get("inserted", 0))),
        dropped=int(d.get("dropped", 0)),
        alerts_fired=len(d.get("alerts") or []),
        alerts=alerts,
        plan=str(d.get("plan", "")),
        usage=dict(d.get("usage", {}) or {}),
    )


def send_sync(
    url: str,
    headers: dict,
    payload: dict,
    timeout: float,
    retries: int,
    method: str = "POST",
) -> Any:
    last_exc: Exception = LogguardConnectionError("Unknown error")
    for attempt in range(1, retries + 1):
        try:
            with httpx.Client(timeout=timeout) as client:
                fn = getattr(client, method.lower())
                resp = fn(url, json=payload, headers=headers)

            if resp.status_code not in _RETRY_STATUSES:
                return _raise_for_status(resp)

            last_exc = LogguardConnectionError(f"Server error {resp.status_code}")
            log.warning("Attempt %d/%d failed status=%d", attempt, retries, resp.status_code)

        except (httpx.RequestError, httpx.TimeoutException) as e:
            last_exc = LogguardConnectionError(f"Connection error: {e!r}")
            log.warning("Attempt %d/%d connection error: %r", attempt, retries, e)

        if attempt < retries:
            time.sleep(0.4 * attempt)

    raise last_exc


def send_sync_no_body(
    url: str,
    headers: dict,
    timeout: float,
    retries: int,
    method: str = "GET",
) -> Any:
    """For GET/DELETE requests without a body."""
    last_exc: Exception = LogguardConnectionError("Unknown error")
    for attempt in range(1, retries + 1):
        try:
            with httpx.Client(timeout=timeout) as client:
                fn = getattr(client, method.lower())
                resp = fn(url, headers=headers)

            if resp.status_code not in _RETRY_STATUSES:
                return _raise_for_status(resp)

            last_exc = LogguardConnectionError(f"Server error {resp.status_code}")

        except (httpx.RequestError, httpx.TimeoutException) as e:
            last_exc = LogguardConnectionError(f"Connection error: {e!r}")

        if attempt < retries:
            time.sleep(0.4 * attempt)

    raise last_exc


async def send_async(
    url: str,
    headers: dict,
    payload: dict,
    timeout: float,
    retries: int,
    method: str = "POST",
) -> Any:
    last_exc: Exception = LogguardConnectionError("Unknown error")
    for attempt in range(1, retries + 1):
        try:
            async with httpx.AsyncClient(timeout=timeout) as client:
                fn = getattr(client, method.lower())
                resp = await fn(url, json=payload, headers=headers)

            if resp.status_code not in _RETRY_STATUSES:
                return _raise_for_status(resp)

            last_exc = LogguardConnectionError(f"Server error {resp.status_code}")

        except (httpx.RequestError, httpx.TimeoutException) as e:
            last_exc = LogguardConnectionError(f"Connection error: {e!r}")

        if attempt < retries:
            await asyncio.sleep(0.4 * attempt)

    raise last_exc


async def send_async_no_body(
    url: str,
    headers: dict,
    timeout: float,
    retries: int,
    method: str = "GET",
) -> Any:
    last_exc: Exception = LogguardConnectionError("Unknown error")
    for attempt in range(1, retries + 1):
        try:
            async with httpx.AsyncClient(timeout=timeout) as client:
                fn = getattr(client, method.lower())
                resp = await fn(url, headers=headers)

            if resp.status_code not in _RETRY_STATUSES:
                return _raise_for_status(resp)

            last_exc = LogguardConnectionError(f"Server error {resp.status_code}")

        except (httpx.RequestError, httpx.TimeoutException) as e:
            last_exc = LogguardConnectionError(f"Connection error: {e!r}")

        if attempt < retries:
            await asyncio.sleep(0.4 * attempt)

    raise last_exc
