"""Slices Experiment API Client."""

from collections.abc import Collection
from datetime import datetime
from enum import Enum
from typing import Annotated

from httpx import Client, HTTPError
from msgspec import Meta, Struct
from msgspec.json import Decoder as JsonDecoder

from slices_clientlib_core.base.cache import clear_cache_entry
from slices_clientlib_core.base.exceptions import from_httpx_exception
from slices_clientlib_core.util.pagination import PagedCollection

from .project import ProjectClient


class Experiment(Struct):
    """Experiment."""

    id: str
    project_id: str
    "The project to which this experiment belongs"
    friendly_name: str
    "Human readable name"

    created_at: Annotated[datetime, Meta(tz=True)]
    "Timestamp when the experiment was created"
    created_by: str
    "The user who created this experiment"

    expires_at: datetime
    "Timestamp when the experiment expires."
    archived: bool = False
    "An archived experiment cannot be (re-)used. This is a final state."

    description: str | None = None


_experiment_decoder = JsonDecoder(Experiment)


class ResourceState(str, Enum):
    """The state a resource is in."""

    """
    allowed enum values
    """
    UNKNOWN = "unknown"
    INACTIVE = "inactive"
    RESERVED = "reserved"
    DELETING = "deleting"
    DELETED = "deleted"
    FAILED = "failed"
    IMAGING = "imaging"
    BOOTING = "booting"
    INITIALIZING = "initializing"
    STARTING = "starting"
    UP = "up"
    POWEROFF = "poweroff"
    REBOOTING = "rebooting"


class ExperimentResource(Struct):
    """Experiment Resource."""

    id: str

    created_at: Annotated[datetime, Meta(tz=True)]
    "Timestamp when the experiment was created"
    created_by: str
    "The user who created this experiment"
    experiment_id: str
    "TypeID of an Experiment where this resource is a part of"

    friendly_name: str
    "Human readable name"
    project_id: str
    "TypeID of the project"
    service_id: str
    "The ID of the service"
    infra_id: str
    "The ID of the infrastructure"
    status: ResourceState

    type: str
    'The type of resource. For BI Service Resources, this is "iot", "baremetal" or "vm".'
    description: str | None = None
    expires_at: datetime | None = None
    "Timestamp when the experiment resource expires."
    terminated_at: Annotated[datetime, Meta(tz=True)] | None = None
    terminated_by: str | None = None


class ExperimentClient:
    """Experiment Client."""

    API_BASE_PATH = "/apis/exp.slices-ri.eu/v1/experiments/"

    def __init__(self, client: Client, project_client: ProjectClient) -> None:
        """Create Experiment Client."""
        self._client = client
        self._project_client = project_client

        assert client.base_url is not None, "Expected base_url to be set"

    def create(
        self, friendly_name: str, expires_at: datetime, user_project_id_token: str, description: str | None = None
    ) -> Experiment:
        """Create experiment."""
        response = self._client.post(
            self.API_BASE_PATH,
            headers={"Accept": "application/json"},
            json={
                "friendly_name": friendly_name,
                "expires_at": expires_at.isoformat(),
                "description": description,
                "user_project_id_token": user_project_id_token,
            },
        )

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return _experiment_decoder.decode(response.content)

    def get(self, experiment_id: str) -> Experiment:
        """Get experiment."""
        response = self._client.get(f"{self.API_BASE_PATH}{experiment_id}", headers={"Accept": "application/json"})

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return _experiment_decoder.decode(response.content)

    def get_or_create(
        self,
        experiment_name: str,
        project_id: str,
        expires_at: datetime,
        user_project_id_token: str,
        description: str | None = None,
    ) -> Experiment:
        """Get or create an experiment, by name.

        Returns an existing experiment with the same friendly_name, if found and active.
        Creates a new experiment otherwise.
        """
        experiments = self.list(
            friendly_name=experiment_name,
            project_id=project_id,
            include_archived=False,
            include_expired=False,
        )
        for existing_experiment in experiments:
            if experiment_name == existing_experiment.friendly_name:
                if existing_experiment.expires_at < expires_at:
                    # Extend expiration time if the existing experiment expires before requested expire time
                    self.update(
                        existing_experiment.id,
                        project_id,
                        expires_at=expires_at,
                        description=existing_experiment.description,
                    )
                return existing_experiment

        return self.create(experiment_name, expires_at, user_project_id_token, description)

    def get_jwt(self, experiment_id: str) -> str:
        """Get experiment JWT."""
        response = self._client.get(f"{self.API_BASE_PATH}{experiment_id}", headers={"Accept": "application/jwt"})

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return response.text

    def update(
        self, experiment_id: str, project_id: str, *, description: str | None = None, expires_at: datetime | None = None
    ) -> Experiment:
        """Update experiment."""

        if expires_at and expires_at.tzinfo is None:
            raise ValueError("expires_at must be timezone aware")

        update_data = {
            field_name: field_value
            for field_name, field_value in {
                "description": description,
                "expires_at": expires_at.isoformat() if expires_at else None,
            }.items()
            if field_value is not None
        }

        update_data["project_token"] = self._project_client.get_jwt(project_id)

        response = self._client.put(
            f"{self.API_BASE_PATH}{experiment_id}",
            json=update_data,
        )

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            clear_cache_entry("experiment_token", experiment_id)
            return _experiment_decoder.decode(response.content)

    def list(
        self,
        *,
        include_expired: bool | None = False,
        include_archived: bool | None = False,
        project_id: str | None = None,
        friendly_name: str | None = None,
    ) -> Collection[Experiment]:
        """List experiments."""

        params = {
            param_name: param_value
            for param_name, param_value in {
                "include_expired": include_expired,
                "include_archived": include_archived,
                "project_id": project_id,
                "friendly_name": friendly_name,
            }.items()
            if param_value is not None
        }

        return PagedCollection("experiment", Experiment, self.API_BASE_PATH, self._client, params=params)

    def list_resources(
        self, experiment_id: str, *, include_expired: bool | None = None, include_terminated: bool | None = None
    ):
        """List resources for an experiment."""

        params = {
            param_name: param_value
            for param_name, param_value in {
                "include_expired": include_expired,
                "include_terminated": include_terminated,
            }.items()
            if param_value is not None
        }

        return PagedCollection(
            "experiment_resource",
            ExperimentResource,
            f"{self.API_BASE_PATH}{experiment_id}/resources/",
            self._client,
            params=params,
        )

    def delete(self, experiment_id: str) -> Experiment:
        """Delete experiment."""

        response = self._client.delete(f"{self.API_BASE_PATH}{experiment_id}")

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return _experiment_decoder.decode(response.content)

    def member(self, experiment_id: str) -> "ExperimentMemberClient":
        """Get Slices Experiment Member Client."""

        return ExperimentMemberClient(self._client, experiment_id)


class ExperimentMemberRole(str, Enum):
    """Experiment Member Role."""

    VIEWER = "viewer"
    MEMBER = "member"
    ADMIN = "admin"
    LEAD = "lead"


class ExperimentMember(Struct):
    """Experiment Member."""

    role: ExperimentMemberRole
    experiment_id: str
    user_id: str


_experiment_member_decoder = JsonDecoder(ExperimentMember)


class ExperimentMemberClient:
    """Experiment Members Client."""

    API_BASE_PATH = "/apis/exp.slices-ri.eu/v1/experiments/"

    def __init__(self, client: Client, experiment_id: str) -> None:
        """Create Experiment Client."""
        self._client = client
        self.experiment_id = experiment_id

        assert client.base_url is not None, "Expected base_url to be set"

    def list(self) -> Collection[ExperimentMember]:
        """List Experiment Members."""

        return PagedCollection(
            "experiment_member", ExperimentMember, f"{self.API_BASE_PATH}{self.experiment_id}/members/", self._client
        )

    def create(self, user_id: str, role: ExperimentMemberRole | str) -> ExperimentMember:
        """Add experiment member."""

        response = self._client.post(
            f"{self.API_BASE_PATH}{self.experiment_id}/members/",
            json={
                "user_id": user_id,
                "role": role.value if isinstance(role, ExperimentMemberRole) else str(role).lower(),
            },
        )

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return _experiment_member_decoder.decode(response.content)

    def update(self, user_id: str, role: ExperimentMemberRole | str) -> ExperimentMember:
        """Update experiment member role."""

        response = self._client.put(
            f"{self.API_BASE_PATH}{self.experiment_id}/members/{user_id}",
            json={
                "role": role.value if isinstance(role, ExperimentMemberRole) else str(role).lower(),
            },
        )

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return _experiment_member_decoder.decode(response.content)

    def delete(self, user_id: str) -> ExperimentMember:
        """Delete experiment member."""
        response = self._client.delete(
            f"{self.API_BASE_PATH}{self.experiment_id}/members/{user_id}",
        )

        try:
            response.raise_for_status()
        except HTTPError as err:
            raise from_httpx_exception(err) from err
        else:
            return _experiment_member_decoder.decode(response.content)
