"""APIs."""

from datetime import datetime

import humanize
import requests
import typer
from pytz import UTC
from slices_clientlib_core import SlicesClient, SlicesExperimentClient, SlicesProjectClient
from slices_clientlib_core.base.config import SlicesCentralApiClientsConfig
from slices_clientlib_core.base.const import BASE_HTTP_TIMEOUT_S
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 slices_cli_core import console, err_console
from slices_cli_core.util.time import friendly_datetime


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


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

    auth_token: OAuthToken | None

    _slices_client: SlicesClient

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

        self._slices_client = SlicesClient(central_config=central_config)

    @property
    def central_config(self) -> SlicesCentralApiClientsConfig:
        """Return the Slices central config object."""
        return self._slices_client._central_api_clients_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) -> SlicesProjectClient:
        """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=BASE_HTTP_TIMEOUT_S,
        )
        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=BASE_HTTP_TIMEOUT_S,
        )
        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
