from __future__ import annotations

from typing import Any

import httpx

from ._version import __version__
from .auth import ClientCredentialsAuth
from .config import (
    DEFAULT_API_URL,
    DEFAULT_AUTH0_AUDIENCE,
    DEFAULT_AUTH0_DOMAIN,
    load_config,
)
from .errors import ApiError, NotFoundError
from .models import (
    Model,
    ModelGroup,
    ModelRun,
    ModelRunOutputResult,
    ModelRunPage,
    Sensitivity,
    User,
)
from .transport import HttpxSyncTransport, QueryParams, Transport


def _normalize_base_url(base_url: str) -> str:
    return base_url.rstrip("/")


class Client:
    def __init__(
        self,
        *,
        base_url: str | None = None,
        auth0_domain: str | None = None,
        auth0_audience: str | None = None,
        client_id: str,
        client_secret: str,
        timeout: float = 30.0,
        transport: Transport | None = None,
    ) -> None:
        config = load_config()

        resolved_base_url = base_url or config.base_url or DEFAULT_API_URL
        resolved_domain = auth0_domain or config.auth0_domain or DEFAULT_AUTH0_DOMAIN
        resolved_audience = (
            auth0_audience or config.auth0_audience or DEFAULT_AUTH0_AUDIENCE
        )

        self._base_url = _normalize_base_url(resolved_base_url)
        self._auth = ClientCredentialsAuth(
            auth0_domain=resolved_domain,
            audience=resolved_audience,
            client_id=client_id,
            client_secret=client_secret,
        )

        self._transport = transport or HttpxSyncTransport(
            client=httpx.Client(timeout=timeout)
        )

    def close(self) -> None:
        self._transport.close()

    def __enter__(self) -> Client:
        return self

    def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
        self.close()

    def get_models(self, *, time_horizon: str | None = None) -> list[Model]:
        params: dict[str, Any] = {}
        if time_horizon is not None:
            params["timeHorizon"] = time_horizon

        data = self._request_json("GET", "/model", params=params)
        return [Model.model_validate(item) for item in data]

    def get_model(self, model_id: str) -> Model:
        data = self._request_json("GET", f"/model/{model_id}")
        return Model.model_validate(data)

    def get_model_sensitivities(self, model_id: str) -> list[Sensitivity]:
        data = self._request_json("GET", f"/model/{model_id}/sensitivities")
        return [Sensitivity.model_validate(item) for item in data]

    def get_current_user(self) -> User:
        data = self._request_json("GET", "/user/current")
        return User.model_validate(data)

    def get_model_groups(self) -> list[ModelGroup]:
        data = self._request_json("GET", "/model-group")
        return [ModelGroup.model_validate(item) for item in data]

    def get_model_group(self, model_group_id: str) -> ModelGroup:
        data = self._request_json("GET", f"/model-group/{model_group_id}")
        return ModelGroup.model_validate(data)

    def get_model_group_models(self, model_group_id: str) -> list[Model]:
        data = self._request_json("GET", f"/model-group/{model_group_id}/models")
        return [Model.model_validate(item) for item in data]

    def get_model_runs(
        self,
        model_id: str,
        *,
        limit: int | None = None,
        cursor: str | None = None,
        after: str | None = None,
        before: str | None = None,
        status: str | list[str] | None = None,
        sensitivity: str | None = None,
    ) -> ModelRunPage:
        params: dict[str, Any] = {}
        if limit is not None:
            params["limit"] = limit
        if cursor is not None:
            params["cursor"] = cursor
        if after is not None:
            params["after"] = after
        if before is not None:
            params["before"] = before
        if status is not None:
            params["status"] = status if isinstance(status, str) else ",".join(status)
        if sensitivity is not None:
            params["sensitivity"] = sensitivity
        data = self._request_json("GET", f"/model/{model_id}/run", params=params)
        return ModelRunPage.model_validate(data)

    def get_model_run(self, model_id: str, run_id: str) -> ModelRun:
        data = self._request_json("GET", f"/model/{model_id}/run/{run_id}")
        return ModelRun.model_validate(data)

    def get_model_run_output(
        self,
        model_id: str,
        model_run_id: str,
        *,
        comparison_model: str | list[str] | None = None,
    ) -> ModelRunOutputResult:
        params: QueryParams = None
        if comparison_model is not None:
            models = (
                [comparison_model]
                if isinstance(comparison_model, str)
                else comparison_model
            )
            params = [("comparisonModel", m) for m in models]
        data = self._request_json(
            "GET",
            f"/model/{model_id}/run/{model_run_id}/output",
            params=params,
        )
        return ModelRunOutputResult.model_validate(data)

    def get_latest_model(self, model_group_id: str) -> Model:
        """Get the latest revision model in a model group."""
        models = self.get_model_group_models(model_group_id)
        if not models:
            raise NotFoundError("no models found in model group")
        return max(models, key=lambda m: m.revision)

    def get_latest_model_run(
        self,
        model_id: str,
        *,
        sensitivity: str | None = None,
    ) -> ModelRun:
        """Get the most recent completed run for a model.

        By default returns the latest base run (excluding sensitivities).
        Pass a sensitivity ID to get the latest run for that sensitivity.
        """
        resolved_sensitivity = sensitivity if sensitivity is not None else "base"
        page = self.get_model_runs(
            model_id, limit=1, status="completed", sensitivity=resolved_sensitivity
        )
        if not page.data:
            raise NotFoundError("no completed runs found for model")
        return page.data[0]

    def get_latest_model_run_output(
        self,
        model_id: str,
        *,
        sensitivity: str | None = None,
    ) -> ModelRunOutputResult:
        """Get the output from the most recent completed run for a model.

        By default returns the output from the latest base run.
        Pass a sensitivity ID to get the output for that sensitivity's latest run.
        """
        latest_run = self.get_latest_model_run(model_id, sensitivity=sensitivity)
        return self.get_model_run_output(model_id, latest_run.id)

    def _request_json(
        self,
        method: str,
        path: str,
        *,
        params: QueryParams = None,
    ) -> Any:
        url = f"{self._base_url}{path}"
        token = self._auth.get_access_token(self._transport)

        response = self._transport.request(
            method,
            url,
            headers={
                "authorization": f"Bearer {token}",
                "accept": "application/json",
                "x-sdk-version": f"python-sdk/{__version__}",
            },
            params=params,
        )

        if response.status_code == 404:
            raise NotFoundError("resource not found")

        if response.status_code >= 400:
            body: Any | None
            try:
                body = response.json()
            except Exception:
                body = response.text

            raise ApiError(
                status_code=response.status_code,
                message="request failed",
                body=body,
            )

        try:
            return response.json()
        except ValueError as e:
            raise ApiError(
                status_code=response.status_code,
                message=f"invalid JSON response: {e}",
                body=response.text,
            ) from e
