"""
AgniPod SDK — HTTP Client
===========================

Thin wrapper around ``requests`` with:
- automatic auth header injection
- structured error handling (maps HTTP codes → typed exceptions)
- configurable retries with exponential back-off
- SSE streaming support
"""

from __future__ import annotations

import json
import time
from typing import Any, Dict, Iterator, Optional

import requests

from ._config import (
    BASE_URL,
    DEFAULT_MAX_RETRIES,
    DEFAULT_TIMEOUT,
    DEFAULT_STREAM_TIMEOUT,
    RETRY_BACKOFF_FACTOR,
    RETRY_STATUS_CODES,
    USER_AGENT,
    resolve_api_key,
)
from ._exceptions import (
    AgniPodError,
    ConfigurationError,
    APIConnectionError,
    APITimeoutError,
    StreamError,
    raise_for_status,
)
from ._types import StreamChunk


class HttpClient:
    """Low-level HTTP transport used by every resource class."""

    def __init__(
        self,
        api_key: str | None = None,
        timeout: float | None = None,
        max_retries: int | None = None,
    ):
        self.api_key = resolve_api_key(api_key)
        self.base_url = BASE_URL
        self.timeout = timeout or DEFAULT_TIMEOUT
        self.max_retries = max_retries if max_retries is not None else DEFAULT_MAX_RETRIES

        if not self.api_key:
            raise ConfigurationError(
                "No API key provided. Set it via:\n"
                "  1. AgniPod(api_key='agni_...')\n"
                "  2. AGNIPOD_API_KEY environment variable\n"
                "  3. Run: agnipod login"
            )

        self._session = requests.Session()
        self._session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "X-API-Key": self.api_key,
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": USER_AGENT,
        })

    # ── Core request ─────────────────────────────────────────────────────

    def request(
        self,
        method: str,
        path: str,
        *,
        json_data: Dict[str, Any] | None = None,
        params: Dict[str, Any] | None = None,
        timeout: float | None = None,
        stream: bool = False,
        raw_response: bool = False,
    ) -> Any:
        """Execute an HTTP request with retry logic."""
        url = f"{self.base_url}{path}"
        effective_timeout = timeout or (DEFAULT_STREAM_TIMEOUT if stream else self.timeout)

        last_exc: Exception | None = None

        for attempt in range(self.max_retries + 1):
            try:
                resp = self._session.request(
                    method,
                    url,
                    json=json_data,
                    params=_clean_params(params),
                    timeout=effective_timeout,
                    stream=stream,
                )

                if resp.status_code in RETRY_STATUS_CODES and attempt < self.max_retries:
                    _backoff(attempt)
                    continue

                if raw_response:
                    if not resp.ok:
                        body = _safe_json(resp)
                        raise_for_status(resp.status_code, body)
                    return resp

                body = _safe_json(resp)
                raise_for_status(resp.status_code, body)
                return body

            except (requests.ConnectionError, requests.exceptions.ConnectionError) as exc:
                last_exc = exc
                if attempt < self.max_retries:
                    _backoff(attempt)
                    continue
                raise APIConnectionError(
                    f"Could not connect to {self.base_url}: {exc}"
                ) from exc

            except requests.Timeout as exc:
                last_exc = exc
                if attempt < self.max_retries:
                    _backoff(attempt)
                    continue
                raise APITimeoutError(
                    f"Request timed out after {effective_timeout}s"
                ) from exc

            except AgniPodError:
                raise

            except Exception as exc:
                raise AgniPodError(f"Unexpected error: {exc}") from exc

        raise AgniPodError(
            f"Request failed after {self.max_retries + 1} attempts: {last_exc}"
        )

    # ── Convenience methods ──────────────────────────────────────────────

    def get(self, path: str, **kwargs) -> Any:
        return self.request("GET", path, **kwargs)

    def post(self, path: str, **kwargs) -> Any:
        return self.request("POST", path, **kwargs)

    def put(self, path: str, **kwargs) -> Any:
        return self.request("PUT", path, **kwargs)

    def patch(self, path: str, **kwargs) -> Any:
        return self.request("PATCH", path, **kwargs)

    def delete(self, path: str, **kwargs) -> Any:
        return self.request("DELETE", path, **kwargs)

    # ── File upload (multipart/form-data) ────────────────────────────────

    def upload_file(
        self,
        path: str,
        *,
        file_field: str,
        file_name: str,
        file_content: bytes,
        content_type: str = "application/octet-stream",
        form_data: Dict[str, Any] | None = None,
    ) -> Any:
        """POST a multipart/form-data request (for file uploads).

        Does not pass ``Content-Type: application/json`` — lets ``requests``
        set the correct multipart boundary automatically.
        """
        url = f"{self.base_url}{path}"
        files = {file_field: (file_name, file_content, content_type)}
        try:
            resp = self._session.post(
                url,
                files=files,
                data=form_data or {},
                timeout=self.timeout,
            )
            body = _safe_json(resp)
            raise_for_status(resp.status_code, body)
            return body
        except AgniPodError:
            raise
        except requests.ConnectionError as exc:
            raise APIConnectionError(f"Upload connection failed: {exc}") from exc
        except requests.Timeout as exc:
            raise APITimeoutError(f"Upload timed out: {exc}") from exc
        except Exception as exc:
            raise AgniPodError(f"Upload error: {exc}") from exc

    # ── SSE streaming ────────────────────────────────────────────────────

    def stream_sse(self, path: str, json_data: Dict[str, Any]) -> Iterator[StreamChunk]:
        """POST with ``stream=True`` and yield parsed SSE chunks."""
        url = f"{self.base_url}{path}"

        try:
            resp = self._session.post(
                url,
                json=json_data,
                stream=True,
                timeout=DEFAULT_STREAM_TIMEOUT,
                headers={"Accept": "text/event-stream"},
            )

            if not resp.ok:
                body = _safe_json(resp)
                raise_for_status(resp.status_code, body)

            for line in resp.iter_lines(decode_unicode=True):
                if not line or not line.startswith("data: "):
                    continue
                raw = line[6:]
                if raw.strip() == "[DONE]":
                    return
                try:
                    chunk_data = json.loads(raw)
                except json.JSONDecodeError:
                    continue

                if "error" in chunk_data and not chunk_data.get("done"):
                    err_info = chunk_data["error"]
                    msg = (
                        err_info.get("message", str(err_info))
                        if isinstance(err_info, dict)
                        else str(err_info)
                    )
                    raise StreamError(
                        msg,
                        error_type=(
                            err_info.get("type")
                            if isinstance(err_info, dict)
                            else None
                        ),
                    )

                yield StreamChunk(chunk_data)

                if chunk_data.get("done"):
                    return

        except AgniPodError:
            raise
        except requests.ConnectionError as exc:
            raise APIConnectionError(f"Stream connection failed: {exc}") from exc
        except requests.Timeout as exc:
            raise APITimeoutError(f"Stream timed out: {exc}") from exc
        except Exception as exc:
            raise StreamError(f"Stream error: {exc}") from exc

    # ── Lifecycle ────────────────────────────────────────────────────────

    def close(self):
        self._session.close()

    def __del__(self):
        try:
            self._session.close()
        except Exception:
            pass


# ── Module-private helpers ───────────────────────────────────────────────

def _clean_params(params: Dict[str, Any] | None) -> Dict[str, Any] | None:
    """Strip None values so they don't appear as ``?key=None`` in the URL."""
    if params is None:
        return None
    return {k: v for k, v in params.items() if v is not None}


def _safe_json(resp: requests.Response) -> Any:
    """Best-effort JSON parse; fall back to text or empty dict."""
    try:
        return resp.json()
    except (json.JSONDecodeError, ValueError):
        text = resp.text.strip()
        return {"detail": text} if text else {}


def _backoff(attempt: int):
    """Exponential back-off."""
    delay = RETRY_BACKOFF_FACTOR * (2 ** attempt)
    time.sleep(delay)
