"""APIs."""

from datetime import datetime

import humanize
import requests
import typer
from pytz import UTC
from slices_clientlib_aiis import SlicesClientWithAIInfraService, SlicesProjectClientWithAIInfraService
from slices_clientlib_aiis.base.config import SlicesAIApiClientsConfig
from slices_clientlib_core import SlicesExperimentClient
from slices_clientlib_core.base.config import SlicesCentralApiClientsConfig
from slices_clientlib_core.base.exceptions import ForbiddenException
from slices_clientlib_core.experiment import Experiment
from slices_clientlib_core.oauth2.config import load_oauth_config
from slices_clientlib_core.oauth2.token import OAuthToken
from slices_clientlib_core.project import Project

from . import console, err_console
from .utils import friendly_datetime


class NotLoggedInException(Exception):
    """Not logged in."""


class ApiClients:
    """API Clients."""

    auth_token: OAuthToken | None

    _slices_client: SlicesClientWithAIInfraService

    def __init__(
        self,
        auth_token: OAuthToken | None,
        central_config: SlicesCentralApiClientsConfig | str | None = None,
        ai_config: SlicesAIApiClientsConfig | str | None = None,
    ):
        """Create API clients."""
        self.auth_token = auth_token
        self.oauth_config = load_oauth_config()

        self._slices_client = SlicesClientWithAIInfraService(central_config, ai_config)

    @property
    def central_config(self) -> SlicesCentralApiClientsConfig:
        """Return the Slices central config object."""
        return self._slices_client._central_api_clients_config

    @property
    def ai_config(self) -> SlicesAIApiClientsConfig:
        """Return the Slices Basic Infrastructure config object."""
        return self._slices_client.ai_api_clients_config

    # TODO can we prevent the copy paste here?

    def reload_ai_config(self) -> bool:
        """Reload the Slices AI Infrastructure Service config object from the default online source.

        This is useful because it is locally cached.

        This does nothing if the bi slices config was not loaded from
        the default online source or its cached version.

        Returns: True if reload was done, False if reload was skipped.
        """
        return self._slices_client.reload_ai_config()

    @property
    def public_key(self):
        """Pubkey API."""
        return self._slices_client.public_key

    @property
    def experiment(self):
        """Experiment API."""
        return self._slices_client.experiment

    @property
    def project(self):
        """Project API."""
        return self._slices_client.project

    @property
    def user(self):
        """User API."""
        return self._slices_client.user

    def for_project(self, project_id: str) -> SlicesProjectClientWithAIInfraService:
        """Slices Project Client."""
        return self._slices_client.for_project(project_id)

    def for_experiment(self, experiment_id: str, project_id: str) -> SlicesExperimentClient:
        """Slices Experiment Client."""
        return self._slices_client.for_experiment(experiment_id, project_id)

    def get_id_token_for_audience(self, audience: str | list[str], project_id: str):
        """Return JWT ID token of logged in user."""
        if not self.auth_token:
            raise NotLoggedInException

        data = {
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "subject_token": self.auth_token.access_token,
            "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "client_id": self.oauth_config.client_id,
            "client_secret": self.oauth_config.client_secret,
            "requested_token_type": "urn:ietf:params:oauth:token-type:id_token",
            "audience": audience,
            "scope": f"{self.auth_token.scope} project:{project_id}",
        }

        response = requests.post(
            url=self.oauth_config.token_endpoint_url,
            data=data,
            timeout=10,
        )
        response.raise_for_status()
        return response.json()["access_token"]

    def get_access_token_for_audience(self, audience: str | list[str], project_id: str | None = None):
        """Return JWT access token of logged in user for target resource."""
        if not self.auth_token:
            raise NotLoggedInException

        scope = self.auth_token.scope
        if project_id:
            scope = f"{scope} project:{project_id}"

        data = {
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "subject_token": self.auth_token.access_token,
            "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "client_id": self.oauth_config.client_id,
            "client_secret": self.oauth_config.client_secret,
            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "audience": audience,
            "scope": scope,
        }

        response = requests.post(
            url=self.oauth_config.token_endpoint_url,
            data=data,
            timeout=10,
        )
        response.raise_for_status()
        return response.json()["access_token"]

    def get_experiment(self, experiment_name: str, *, project_id: str, must_be_active=False) -> Experiment | None:
        """Get experiment by name."""
        return next(
            iter(
                self.experiment.list(
                    friendly_name=experiment_name,
                    project_id=project_id,
                    include_archived=not must_be_active,
                    include_expired=not must_be_active,
                )
            ),
            None,
        )

    def get_experiment_by_id(self, experiment_id: str) -> Experiment:
        """Get Experiment by ID."""
        return self.experiment.get(experiment_id)

    def get_or_create_experiment(self, experiment_name: str, project_id: str, expires_at: datetime) -> Experiment:
        """Get or create experiment by name."""
        experiment = self.get_experiment(
            experiment_name,
            project_id=project_id,
            must_be_active=True,
        )

        if not experiment:
            # Create it

            user_project_id_token = self.get_id_with_project_jwt(
                self.central_config.central_hub_services.experiment_authority.audience, project_id
            )

            # First make sure project has not yet expired.
            project = self.project.get(project_id)
            if project and project.expires_at and project.expires_at < datetime.now(UTC):
                hum_ago = humanize.precisedelta(
                    project.expires_at - datetime.now(UTC), minimum_unit="hours", format="%d"
                )
                console.print(
                    f"[red]:clock10: Project expired on {friendly_datetime(project.expires_at)} ({hum_ago} ago)."
                )
                raise typer.Exit(code=1) from None

            experiment = self.experiment.create(
                friendly_name=experiment_name,
                description=None,
                expires_at=expires_at,
                user_project_id_token=user_project_id_token,
            )

        return experiment

    def get_experiment_jwt(self, experiment_id: str) -> str:
        """Get Experiment JWT."""
        try:
            return self.experiment.get_jwt(experiment_id)
        except ForbiddenException as err:
            err_console.print(
                f"[red]You have insufficient permissions to create a token for experiment [i]{experiment_id}[/] . :x:"
            )
            raise typer.Exit(1) from err

    def get_project_jwt(self, project_id: str) -> str:
        """Get Project JWT."""

        try:
            return self.project.get_jwt(project_id)
        except ForbiddenException as err:
            err_console.print(
                f"[red]You have insufficient permissions to create a token for project [i]{project_id}[/] . :x:"
            )
            raise typer.Exit(1) from err

    def get_id_with_project_jwt(self, audience: str, project_id: str) -> str:
        """Get ID Token with project JWT."""

        return self.get_id_token_for_audience(audience, project_id)

    def get_project_by_name(self, project_name: str) -> Project | None:
        """Get project by name."""
        for project in self.project.list():
            if project_name in (project.id, project.name):
                return project
        return None
