"""BI Snapshot CLI."""

import functools
from enum import Enum
from typing import Annotated

import typer
from msgspec.json import encode as json_encode
from msgspec.yaml import encode as yaml_encode
from rich.table import Table
from slices_clientlib_bi.bi_resource import Snapshot
from slices_clientlib_core.base.exceptions import (
    ApiException,
    BadRequestException,
    ForbiddenException,
    ServiceException,
)
from urllib3.connection import HTTPSConnection
from urllib3.exceptions import MaxRetryError, NewConnectionError

from slices_cli_bi import console, err_console
from slices_cli_bi.core.progress import follow_one_progress
from slices_cli_bi.util.errors import get_api_error_detail
from slices_cli_bi.util.time import friendly_datetime

from .bi_context import BIContext
from .bi_helpers import bi_get_experiment, bi_get_resource

snapshots_app = typer.Typer(no_args_is_help=True)


def handle_some_exceptions(func):
    """Handle specific exceptions by giving the user a useful error message instead of a confusing stacktrace."""

    @functools.wraps(func)
    def wrapper_handle_some_exceptions(*args, **kwargs):
        """Function wrapper for handle_some_exceptions."""  # noqa: D401
        try:
            return func(*args, **kwargs)
        except ServiceException as e:
            err_console.print("[red]There was a connection problem. The service might be down. :zap:")
            err_console.print(f"[red]Details: {get_api_error_detail(e)}")
            raise typer.Exit(code=1) from None
        except BadRequestException as e:
            if "Project expired".lower() in e._httpx_status_error.response.text.lower():
                # Typically this occurs on experiment create
                err_console.print("[red]The project has expired. :x:")
                raise typer.Exit(code=1) from None
            # If it's something else, re-raise the exception and handle as usually
            raise
        except MaxRetryError as e:
            if isinstance(e.reason, NewConnectionError | HTTPSConnection):
                # example:
                #   NewConnectionError -> [Failed to establish a new connection: Errno 113] No route to host
                #   HTTPSConnection -> Failed to establish a new connection: [Errno 111] Connection refused
                err_console.print("[red]There was a connection problem. The service might be down. :zap:")
                err_console.print(f"[red]Details: {e.reason}")
                raise typer.Exit(code=1) from None
            # If it's something else, re-raise the exception and handle as usually
            raise

    return wrapper_handle_some_exceptions


@snapshots_app.command(name="create")
def create_snapshot(
    ctx: BIContext,
    name: Annotated[str, typer.Argument(show_default=False, help="Short user readable name/ID")],
    description: Annotated[str, typer.Option(prompt=True, show_default=False, help="Description of this snapshot")],
    experiment: Annotated[
        str,
        typer.Option(..., help="Experiment name or ID.", prompt=True, show_default=False, envvar="SLICES_EXPERIMENT"),
    ],
    resource: Annotated[str, typer.Option(help="Resource name or ID.", show_default=False)],
):
    """Create a snapshot of a resource."""
    try:
        exp = bi_get_experiment(ctx, experiment)
        res = bi_get_resource(ctx, exp, resource)
        ctx.obj.get_bi_resources_api_by_experiment(exp).create_snapshot(res.id, name, description)
        console.print(f"[bold green]:computer: Created snapshot {name!r} :white_check_mark:")
    except BadRequestException as exc:
        err_console.print("[bold red]Could not create snapshot[/] :x:")
        err_console.print(f"[red] {get_api_error_detail(exc)}")
        raise typer.Exit(code=1) from None
    except ForbiddenException:
        err_console.print("[bold red]You do not have permission to create a snapshot.[/] :x:")
        raise typer.Exit(code=1) from None
    except ApiException as exc:
        err_console.print("[bold red]Could not create snapshot[/] :x:")
        err_console.print(f"[red] {get_api_error_detail(exc)}")
        raise typer.Exit(code=1) from None


def _print_snapshots_table(resource_name, snapshots):
    table = Table(
        "Name",
        "Description",
        "Created at",
        "Includes RAM state",
        title=f"Slices BI Snapshots for resource {resource_name!r}",
    )
    for snapshot in snapshots:
        if isinstance(snapshot, Snapshot):
            table.add_row(
                snapshot.name,
                snapshot.description,
                friendly_datetime(snapshot.created) if snapshot.created else "(unknown)",
                str(snapshot.includes_ram) if snapshot.includes_ram is not None else "(unknown)",
            )

    console.print(table)


class ListFormat(Enum):
    """List Formats."""

    table = "table"
    json = "json"
    yaml = "yaml"


@snapshots_app.command(name="list", rich_help_panel="Commands")
@handle_some_exceptions
def list_snapshots(
    ctx: BIContext,
    experiment: Annotated[
        str,
        typer.Option(..., help="Experiment name or ID.", prompt=True, show_default=False, envvar="SLICES_EXPERIMENT"),
    ],
    resource: Annotated[str, typer.Option(help="Resource name or ID.", show_default=False)],
    format: Annotated[
        ListFormat,
        typer.Option("--format", "-f", help="Format in which the snapshots should be listed."),
    ] = "table",  # type: ignore
):
    """List snapshots."""
    exp = bi_get_experiment(ctx, experiment)
    res = bi_get_resource(ctx, exp, resource)
    snapshots = ctx.obj.get_bi_resources_api_by_experiment(exp).list_snapshots(res.id)

    if format == ListFormat.table:
        _print_snapshots_table(resource, snapshots)
    elif format == ListFormat.json:
        console.print_json(json_encode(list(snapshots)).decode())
    elif format == ListFormat.yaml:
        console.print(yaml_encode(list(snapshots)).decode())


@snapshots_app.command("revert", rich_help_panel="Commands")
@handle_some_exceptions
def revert_to_snapshot(
    ctx: BIContext,
    name: Annotated[str, typer.Argument(show_default=False, help="Name of the snapshot to revert to")],
    experiment: Annotated[
        str,
        typer.Option(..., help="Experiment name or ID.", prompt=True, show_default=False, envvar="SLICES_EXPERIMENT"),
    ],
    resource: Annotated[str, typer.Option(help="Resource name or ID.", show_default=False)],
):
    """Revert the resource to a specific snapshot.

    This is a destructive operation! Data will be wiped!
    """
    exp = bi_get_experiment(ctx, experiment)
    res = bi_get_resource(ctx, exp, resource)

    tasks = ctx.obj.get_bi_resources_api_by_experiment(exp).revert_to_snapshot(res.id, name)

    project_id = ctx.obj.get_current_project_id()
    if not project_id:
        raise typer.Exit(0)

    follow_one_progress(
        ctx.obj.bi_tasks_api,
        [task.id for task in tasks],
        [f"Reverting {resource} to initial snapshot"] * len(tasks),
    )
