"""Medelement MIS API client."""

from __future__ import annotations

import base64
import logging
import time
from types import TracebackType
from typing import Any

import httpx

from medelement_mis.config import (
    DEFAULT_RETRIES,
    DEFAULT_RETRY_BACKOFF,
    MedelementConfig,
)
from medelement_mis.exceptions import APIError, AuthenticationError, NotFoundError, ValidationError
from medelement_mis.resources.nomenclatures import NomenclaturesAPI
from medelement_mis.resources.patients import PatientsAPI
from medelement_mis.resources.receptions import ReceptionsAPI
from medelement_mis.resources.specialists import SpecialistsAPI
from medelement_mis.resources.timetable import TimetableAPI

logger = logging.getLogger("medelement_mis")
RETRYABLE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})


class MedelementClient:
    """Client for Medelement MIS API.

    Uses Basic HTTP authentication (RFC 7617) with ``X-Integrator-Key`` header
    and reuses a shared ``httpx.Client`` for connection pooling.
    """

    def __init__(
        self,
        config: MedelementConfig | None = None,
        *,
        base_url: str | None = None,
        user_id: str | None = None,
        password: str | None = None,
        integrator_key: str | None = None,
        timeout: float | httpx.Timeout | None = None,
        retries: int | None = None,
        retry_backoff: float | None = None,
        http_client: httpx.Client | None = None,
    ) -> None:
        if config is not None:
            base_url = base_url or config.base_url
            user_id = user_id or config.user_id
            password = password or config.password
            integrator_key = integrator_key or config.integrator_key
            timeout = timeout if timeout is not None else config.timeout
            retries = retries if retries is not None else config.retries
            retry_backoff = (
                retry_backoff if retry_backoff is not None else config.retry_backoff
            )

        missing = [
            name
            for name, value in (
                ("base_url", base_url),
                ("user_id", user_id),
                ("password", password),
                ("integrator_key", integrator_key),
            )
            if value is None
        ]
        if missing:
            missing_str = ", ".join(missing)
            raise ValueError(f"Missing required client configuration values: {missing_str}")

        assert base_url is not None
        assert user_id is not None
        assert password is not None
        assert integrator_key is not None

        self.base_url = base_url.rstrip("/")
        self._user_id = user_id
        self._password = password
        self._integrator_key = integrator_key
        self.timeout = timeout if timeout is not None else (config.timeout if config else 10.0)
        self.retries = max(retries if retries is not None else DEFAULT_RETRIES, 0)
        self.retry_backoff = max(
            retry_backoff if retry_backoff is not None else DEFAULT_RETRY_BACKOFF,
            0.0,
        )
        self._auth_headers = self._build_auth_headers()
        self._owns_http_client = http_client is None
        self._http = http_client or httpx.Client(
            base_url=self.base_url,
            headers=self._auth_headers,
            timeout=self.timeout,
            event_hooks={
                "request": [self._log_request],
                "response": [self._log_response],
            },
        )

        self.patients = PatientsAPI(self)
        self.specialists = SpecialistsAPI(self)
        self.timetable = TimetableAPI(self)
        self.receptions = ReceptionsAPI(self)
        self.nomenclatures = NomenclaturesAPI(self)

    def _build_auth_headers(self) -> dict[str, str]:
        credentials = f"{self._user_id}:{self._password}"
        encoded = base64.b64encode(credentials.encode()).decode()
        return {
            "Authorization": f"Basic {encoded}",
            "X-Integrator-Key": self._integrator_key,
        }

    @property
    def auth_headers(self) -> dict[str, str]:
        """Get authentication headers for API requests."""
        return dict(self._auth_headers)

    def _log_request(self, request: httpx.Request) -> None:
        logger.debug("HTTP request %s %s", request.method, request.url)

    def _log_response(self, response: httpx.Response) -> None:
        request = response.request
        logger.debug(
            "HTTP response %s %s -> %s",
            request.method,
            request.url,
            response.status_code,
        )

    def _should_retry(self, method: str, response: httpx.Response | None = None) -> bool:
        if method.upper() not in RETRYABLE_METHODS:
            return False
        return response is None or response.status_code >= 500

    def _request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
        last_exception: httpx.HTTPError | None = None
        max_attempts = self.retries + 1

        for attempt in range(max_attempts):
            try:
                response = self._http.request(method, url, **kwargs)
            except (httpx.TimeoutException, httpx.TransportError) as exc:
                last_exception = exc
                if attempt == self.retries or not self._should_retry(method):
                    raise

                delay = self.retry_backoff * (2**attempt)
                logger.debug(
                    "Retrying %s %s after transport error (%s/%s, %.2fs)",
                    method.upper(),
                    url,
                    attempt + 1,
                    max_attempts,
                    delay,
                )
                time.sleep(delay)
                continue

            if attempt < self.retries and self._should_retry(method, response):
                delay = self.retry_backoff * (2**attempt)
                logger.debug(
                    "Retrying %s %s after HTTP %s (%s/%s, %.2fs)",
                    method.upper(),
                    url,
                    response.status_code,
                    attempt + 1,
                    max_attempts,
                    delay,
                )
                time.sleep(delay)
                continue

            return response

        if last_exception is not None:
            raise last_exception
        raise RuntimeError("Request retry loop exited unexpectedly")

    def _extract_error_message(self, response: httpx.Response) -> str:
        try:
            payload: Any = response.json()
        except ValueError:
            payload = response.text.strip()

        if isinstance(payload, dict):
            for key in ("message", "detail", "error", "errors"):
                value = payload.get(key)
                if value:
                    return str(value)
            return str(payload)

        if isinstance(payload, list):
            return str(payload)

        if payload:
            return str(payload)

        return response.text.strip()

    def _raise_for_status(self, response: httpx.Response) -> None:
        if response.is_success:
            return

        message = self._extract_error_message(response)
        if not message:
            message = (
                "The Medelement API returned an empty error response. "
                "This is usually an upstream service error."
            )

        if response.status_code == 401:
            if self._integrator_key and message == "[]":
                raise AuthenticationError(
                    "401 Unauthorized: [] response received even though X-Integrator-Key was provided."
                )
            raise AuthenticationError(f"401 Unauthorized: {message or 'Authentication failed.'}")

        if response.status_code == 403:
            raise AuthenticationError(
                message or "403 Forbidden: Access denied. Please contact the API provider to request access."
            )

        if response.status_code == 404:
            raise NotFoundError(message or "404 Not Found")

        if response.status_code == 422:
            raise ValidationError(message or "422 Unprocessable Entity")

        raise APIError(f"{response.status_code} {response.reason_phrase}: {message}".strip())

    def close(self) -> None:
        """Close the underlying HTTP client if this instance owns it."""
        if self._owns_http_client:
            self._http.close()

    def __enter__(self) -> "MedelementClient":
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        self.close()

    @classmethod
    def from_config(cls, config: MedelementConfig) -> "MedelementClient":
        """Build a client instance from a ``MedelementConfig`` object."""
        return cls(config=config)
