from __future__ import annotations

import json
from typing import Any, Mapping, Optional
from urllib.parse import urljoin

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from specific_ai.platform.exceptions import (
    APIErrorDetails,
    AuthenticationError,
    NetworkError,
    NotFoundError,
    PermissionDeniedError,
    RateLimitError,
    ServerError,
    SpecificAIAPIError,
    ValidationError,
)


class BaseClient:
    """
    Core HTTP client for the SpecificAI platform API.

    This class centralizes:
    - Base URL management
    - Authentication header injection
    - Retries
    - Error handling and exception normalization
    """

    def __init__(
        self,
        *,
        base_url: str,
        api_key: Optional[str] = None,
        api_key_header: str = "Authorization",
        api_key_prefix: Optional[str] = "Bearer",
        timeout_s: float = 30.0,
        max_retries: int = 3,
        backoff_factor: float = 0.5,
        session_cookie: Optional[str] = None,
        session_cookie_name: str = "session",
        default_headers: Optional[Mapping[str, str]] = None,
    ):
        """
        Args:
            base_url: Base URL of the SpecificAI backend (e.g. "https://app.specific.ai").
            api_key: API key or bearer token. If provided, will be sent via `api_key_header`.
            api_key_header: Header name for the API key (default: "Authorization").
            api_key_prefix: Optional prefix, e.g. "Bearer". If None/empty, sends raw value.
            timeout_s: Default request timeout in seconds.
            max_retries: Max retry attempts for transient errors.
            backoff_factor: Exponential backoff factor for retries.
            session_cookie: Optional session cookie value for deployments using cookie auth.
            session_cookie_name: Cookie name for `session_cookie`.
            default_headers: Optional extra headers to send on every request.

        Raises:
            ValueError: If base_url is empty.
        """
        if not base_url or not str(base_url).strip():
            raise ValueError("base_url is required")

        self._base_url = base_url.rstrip("/") + "/"
        self._timeout_s = timeout_s

        self._session = requests.Session()

        retry = Retry(
            total=max_retries,
            connect=max_retries,
            read=max_retries,
            status=max_retries,
            backoff_factor=backoff_factor,
            status_forcelist=(429, 500, 502, 503, 504),
            allowed_methods=frozenset(["GET", "POST", "PUT", "PATCH", "DELETE"]),
            raise_on_status=False,
        )
        adapter = HTTPAdapter(max_retries=retry)
        self._session.mount("http://", adapter)
        self._session.mount("https://", adapter)

        headers: dict[str, str] = {
            "Accept": "application/json",
        }
        if default_headers:
            headers.update(dict(default_headers))

        if api_key:
            token = api_key
            if api_key_prefix:
                token = f"{api_key_prefix} {api_key}"
            headers[api_key_header] = token

        self._session.headers.update(headers)

        if session_cookie is not None:
            self._session.cookies.set(session_cookie_name, session_cookie)

    @property
    def base_url(self) -> str:
        """Returns the configured base URL."""
        return self._base_url

    def request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Mapping[str, Any]] = None,
        json_body: Any = None,
        data: Optional[Mapping[str, Any]] = None,
        files: Any = None,
        headers: Optional[Mapping[str, str]] = None,
        stream: bool = False,
        timeout_s: Optional[float] = None,
    ) -> requests.Response:
        """
        Execute an HTTP request.

        Args:
            method: HTTP method (GET/POST/etc).
            path: Absolute path ("/trainings") or full URL.
            params: Query params.
            json_body: JSON body to send.
            data: Form body to send (for multipart/form-data use with `files`).
            files: `requests` files payload.
            headers: Extra headers for this request only.
            stream: Whether to stream the response.
            timeout_s: Optional request timeout override.

        Returns:
            The raw `requests.Response`. Prefer `request_json()` for JSON APIs.

        Raises:
            NetworkError: For transport-level failures.
            SpecificAIAPIError: For HTTP 4xx/5xx responses.
        """
        url = (
            path
            if path.startswith("http://") or path.startswith("https://")
            else urljoin(self._base_url, path.lstrip("/"))
        )
        try:
            resp = self._session.request(
                method=method.upper(),
                url=url,
                params=params,
                json=json_body,
                data=data,
                files=files,
                headers=dict(headers) if headers else None,
                timeout=timeout_s if timeout_s is not None else self._timeout_s,
                stream=stream,
            )
        except requests.RequestException as e:
            raise NetworkError("Network error calling SpecificAI API", cause=e) from e

        if 200 <= resp.status_code < 300:
            return resp

        raise self._to_api_error(resp)

    def request_json(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Mapping[str, Any]] = None,
        json_body: Any = None,
        data: Optional[Mapping[str, Any]] = None,
        files: Any = None,
        headers: Optional[Mapping[str, str]] = None,
        timeout_s: Optional[float] = None,
    ) -> dict[str, Any] | list[Any]:
        """
        Execute an HTTP request and parse the response as JSON.

        Args:
            method: HTTP method.
            path: Absolute path (e.g. "/trainings") or full URL.
            params: Query params.
            json_body: JSON body to send.
            data: Form fields (for multipart/form-data use with `files`).
            files: Multipart files.
            headers: Extra headers.
            timeout_s: Optional timeout override.

        Returns:
            Parsed JSON (dict or list).

        Raises:
            SpecificAIAPIError: For HTTP errors or invalid JSON responses.
        """
        resp = self.request(
            method,
            path,
            params=params,
            json_body=json_body,
            data=data,
            files=files,
            headers=headers,
            stream=False,
            timeout_s=timeout_s,
        )
        try:
            return resp.json()
        except Exception as e:
            details = APIErrorDetails(
                status_code=resp.status_code,
                message="Invalid JSON response",
                response_json=None,
                response_text=resp.text,
                request_id=resp.headers.get("x-request-id"),
            )
            raise SpecificAIAPIError(details) from e

    def _to_api_error(self, resp: requests.Response) -> SpecificAIAPIError:
        request_id = resp.headers.get("x-request-id")

        parsed_json: Optional[dict[str, Any]] = None
        message = resp.reason or "Request failed"
        text: Optional[str] = None

        try:
            payload = resp.json()
            if isinstance(payload, dict):
                parsed_json = payload
                # Common FastAPI error keys
                message = (
                    str(payload.get("detail"))
                    if payload.get("detail") is not None
                    else (
                        str(payload.get("message"))
                        if payload.get("message") is not None
                        else message
                    )
                )
            else:
                text = json.dumps(payload)
        except Exception:
            text = resp.text

        details = APIErrorDetails(
            status_code=resp.status_code,
            message=message,
            response_json=parsed_json,
            response_text=text,
            request_id=request_id,
        )

        if resp.status_code == 401:
            return AuthenticationError(details)
        if resp.status_code == 403:
            return PermissionDeniedError(details)
        if resp.status_code == 404:
            return NotFoundError(details)
        if resp.status_code == 429:
            return RateLimitError(details)
        if resp.status_code in (400, 422):
            return ValidationError(details)
        if 500 <= resp.status_code <= 599:
            return ServerError(details)
        return SpecificAIAPIError(details)
