"""AI Infrastructure Service CLI - common code."""

import re
from collections.abc import Collection
from enum import Enum

import typer
from slices_clientlib_aiis.schema.job import Job, JobStatus
from slices_clientlib_core.base.exceptions import BadRequestException, NotFoundException

from . import console, err_console
from .ai_context import AIContext


def version_callback(version: bool):
    """Show CLI version."""
    if version:
        from importlib.metadata import version as metadata_version  # noqa: PLC0415

        console.print(f"v{metadata_version('slices-cli-ai')}")
        raise typer.Exit(0)


def get_requested_ai_jobs(
    ctx: AIContext,
    job_ids: list[str] | None,
    *,
    filter_user: bool = False,
    filter_project: bool = False,
    include_pending: bool = False,
    include_running: bool = False,
    include_finished: bool = False,
    include_deleted: bool = False,
) -> Collection[Job]:
    """Return the requested jobs.

    Be careful, if no job_ids are specified, this returns ALL jobs!
    They are returned as a "PagedCollection", which means they will only fetch jobs as required.
    You can use len() to get the total number of jobs, without requesting all of them.
    """

    allowed_states = []
    if include_pending:
        allowed_states.extend((JobStatus.ONHOLD, JobStatus.QUEUED))
    if include_finished:
        allowed_states.extend(
            (
                JobStatus.FINISHED,
                JobStatus.FAILED,
                JobStatus.CANCELLED,
                JobStatus.HALTED,
            )
        )
    if include_running:
        allowed_states.extend(
            (
                JobStatus.RUNNING,
                JobStatus.STARTING,
                JobStatus.MUSTHALT,
                JobStatus.HALTING,
            )
        )
    if include_deleted:
        allowed_states.append(JobStatus.DELETED)

    if not job_ids:  # all resources
        return (
            ctx.obj.api.for_project(ctx.obj.require_project_id())
            .ai(ctx.obj.site_id)
            .jobs.list(
                user_id=None if not filter_user else ctx.obj.get_logged_in_userinfo("sub"),
                project_id=None if not filter_project else ctx.obj.require_project_id(),
                # Note: allowed_states=None means all except DELETED
                allowed_states=allowed_states,
            )
        )

    return [get_ai_job(ctx, job_id) for job_id in job_ids]


def duration_string_to_seconds(duration: str) -> int | None:  # noqa: PLR0911
    """Convert a duration string (e.g. "1h", "45s, "2d", ...) to seconds."""

    if not duration:
        return None
    match = re.match(r"^([0-9.]+) *([a-zA-Z]+)$", duration)
    if not match:
        return None

    num = float(match.group(1)) if "." in match.group(1) else int(match.group(1))
    unit = match.group(2).lower()
    if unit in ("h", "hour", "hours"):
        return int(num * 60 * 60)
    if unit in ("m", "min", "mins", "minute", "minutes"):
        return int(num * 60)
    if unit in ("s", "sec", "secs", "second", "seconds"):
        return int(num)
    if unit in ("day", "days"):
        return int(num * 60 * 60 * 24)
    if unit in ("week", "weeks"):
        return int(num * 60 * 60 * 24 * 7)
    return None
    # Impossible to support because not fixed length: month, year, ...


def validate_duration_option(
    ctx: typer.Context,
    _param: typer.CallbackParam,
    value: str,
) -> str:
    """Validate a textual timedelta (used for typer arguments)."""

    if ctx.resilient_parsing:
        return value

    seconds = duration_string_to_seconds(value)
    if seconds is None:
        raise typer.BadParameter(f"{value!r} is not a valid duration string (e.g. 1h, 45s, 2d, ...).")

    return value


def status_to_color(status: JobStatus) -> str:  # noqa: PLR0911
    """Return a matching color for a JobStatus."""

    if status == JobStatus.QUEUED:
        return ""
    if status == JobStatus.FAILED:
        return "[red]"
    if status in {JobStatus.STARTING, JobStatus.ASSIGNED}:
        return "[blue]"
    if status == JobStatus.RUNNING:
        return "[dark_blue]"
    if status == JobStatus.FINISHED:
        return "[green]"
    if status in {JobStatus.MUSTHALT, JobStatus.HALTING, JobStatus.HALTED}:
        return "[orange]"
    if status in {JobStatus.CANCELLED, JobStatus.DELETED}:
        return "[grey]"
    return ""


def get_ai_job(ctx: AIContext, job_id: str) -> Job:
    """Get a Job from a full or partial ID."""

    try:
        return ctx.obj.api.for_project(ctx.obj.require_project_id()).ai(ctx.obj.site_id).jobs.get(job_id)
    except NotFoundException:
        err_console.print(f"[red] :x: Job {job_id} not found")
        raise typer.Exit(code=1) from None
    except BadRequestException as e:
        err_console.print(f"[red] :x: Problem fetching job {job_id}: {e.detail}")
        raise typer.Exit(code=1) from None


class ShowResourceFormat(Enum):
    """Show resource format Options."""

    text = "text"
    json = "json"
    yaml = "yaml"


def user_id_to_name(ctx: AIContext, user_id: str) -> str:
    """Convert a user ID to a name, if possible."""

    if user_id == ctx.obj.get_logged_in_userinfo("sub"):
        return ctx.obj.get_logged_in_userinfo("preferred_username")

    try:
        user_info = ctx.obj.api.user.get(user_id)
    except:  # noqa: E722
        return user_id
    else:
        return user_info.preferred_username or user_info.email or user_id


def project_id_to_name(ctx: AIContext, project_id: str) -> str:
    """Convert a project ID to a name, if possible."""

    if project_id == ctx.obj.get_current_project_id():
        return ctx.obj.require_project_friendly_name()

    try:
        project_info = ctx.obj.api.project.get(project_id)
    except:  # noqa: E722
        return project_id
    else:
        return project_info.name or project_id
