"""Retrieve info about other users."""

from datetime import datetime

import typer
from msgspec._core import asdict
from pytz import UTC
from slices_clientlib_core.experiment import ExperimentMember, ExperimentMemberRole
from slices_clientlib_core.project import ProjectMember
from slices_clientlib_core.user import User

from slices_cli_bi import err_console
from slices_cli_bi.core.api import ApiClients
from slices_cli_bi.util.type_id import is_type_id, validate_type_id


class EnrichedUser(User):
    """Project Member with extra user info."""

    @property
    def user_id(self) -> str:
        """Alias for id."""
        return self.id

    @property
    def full_name(self) -> str | None:
        """Name of the user."""
        first_name = self.first_name if self.first_name != "-" else None
        last_name = self.last_name if self.last_name != "-" else None
        if first_name and last_name:
            return f"{first_name} {last_name}"
        return first_name or last_name


class EnrichedProjectMember(EnrichedUser):
    """Project Member with extra user info."""

    project_id: str
    role: str


class EnrichedExperimentMember(EnrichedUser):
    """Experiment Member with extra user info."""

    experiment_id: str
    role: ExperimentMemberRole


USER_ID_HELP = (
    "Typically this is the user ID, but any value like a name or email that uniquely identifies "
    "the user in the active project can also be used. "
    ""
    "You can find the user ID of other users with the command: slices project show"
)


class OtherUsersInfo:
    """Helper to fetch info about other users. This caches the info to make future calls faster."""

    # TODO save cache to disk, clean it on "project list" and "experiment member list" and expiration
    #      this way it will be reused between commands for things like removing or adding members and
    #      for searching members by field

    def __init__(self, api: ApiClients):
        self.api = api
        self.cached_info: dict[str, User] = {}

    def try_get_user_info_opt(self, user_id: str) -> User | None:
        """Get the user's info, or None if anything goes wrong fetching it."""
        if user_id in self.cached_info:
            return self.cached_info[user_id]
        try:
            res = self.api.user.get(user_id)
        except Exception as e:  # noqa: E722
            err_console.print(f"Warning: problem fetching extra user info: {e}")
            return None
        else:
            if res:
                self.cached_info[user_id] = res
            return res

    def clear_cache(self):
        """Clear the cache."""
        self.cached_info = {}

    def _enrich_project_member(self, project_id: str, member: ProjectMember) -> EnrichedProjectMember:
        user_info = self.try_get_user_info_fallback(member.user_id)
        return EnrichedProjectMember(**asdict(user_info), project_id=str(project_id), role=str(member.role))

    def _enrich_experiment_member(self, member: ExperimentMember) -> EnrichedExperimentMember:
        user_info = self.try_get_user_info_fallback(member.user_id)
        return EnrichedExperimentMember(**asdict(user_info), experiment_id=str(member.experiment_id), role=member.role)

    def get_project_members(self, project_id: str) -> list[EnrichedProjectMember]:
        """Fetch the project members."""
        project_members = self.api.project.list_members(project_id)
        return [self._enrich_project_member(project_id, m) for m in project_members]

    def resolve_project_member(self, project_id: str, any_id: str) -> str:
        """Find the project member ID.

        If a non-ID is given, look up all members and check if the member can be uniquely identified.
        """
        if is_type_id(any_id):
            return validate_type_id(any_id, required_type="user")
        project_members = self.get_project_members(project_id)

        res: EnrichedProjectMember | None = None

        def handle_match(matching_member: EnrichedProjectMember):
            nonlocal res
            if res is not None:
                # err_console.print(
                #     f"[bold red]Given value ({any_id!r}) does not uniquely identify user within project. :x:"
                # )
                # raise typer.Exit(1)
                raise typer.BadParameter(f"{any_id!r} does not uniquely identify a user within the project")
            res = matching_member

        for member in project_members:
            if (
                member.preferred_username
                and member.preferred_username.lower() == any_id.lower()
                or member.email
                and member.email.lower() == any_id.lower()
                or member.full_name
                and member.full_name.lower() == any_id.lower()
                or member.last_name
                and member.last_name.lower() == any_id.lower()
                or member.first_name
                and member.first_name.lower() == any_id.lower()
            ):
                handle_match(member)

        if res is None:
            # err_console.print(f"[bold red]Could not find user {any_id!r} within project. :x:")
            # raise typer.Exit(1)
            raise typer.BadParameter(f"Could not find user matching {any_id!r} within the project")
        return res.user_id

    def get_experiment_members(self, experiment_id: str) -> list[EnrichedExperimentMember]:
        """Fetch the experiment members."""
        return [self._enrich_experiment_member(m) for m in self.api.experiment.member(experiment_id).list()]

    def try_get_user_info_fallback(self, user_id: str) -> User:
        """Get the user's info, or fall back to empty data if anything goes wrong fetching it."""

        user_info = self.try_get_user_info_opt(user_id)
        if not user_info:
            # fallback
            return User(
                id=user_id,
                affiliation="-",
                created_at=datetime.fromtimestamp(0, tz=UTC),
                email="-",
                first_name="-",
                last_name="-",
                organisation="-",
                preferred_username="-",
            )
        return user_info

    def try_get_user_full_name(self, user_id: str) -> str:
        """Get the user's full name, or fall back to its ID."""

        extra_user_info = self.try_get_user_info_opt(user_id)
        if not extra_user_info:
            return user_id
        return f"{extra_user_info.first_name} {extra_user_info.last_name}"
