"""BI Flavor 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_flavor import AnyFlavor, BareMetalFlavor, BareMetalFlavorCreate, VMFlavor, VMFlavorCreate
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.util.errors import get_api_error_detail
from slices_cli_bi.util.type_id import get_type_id_domain

from .bi_context import BIContext

flavors_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


# Not supported at the moment
# @flavors_app.command(name="create", rich_help_panel="Commands")
# def create_flavor(
#     ctx: BIContext,
#     friendly_name: Annotated[
#         str,
#         typer.Option(
#             prompt=True,
#             show_default=False,
#             help='Short user readable name. On the current BIs, this should start with "custom."',
#         ),
#     ],
#     description: Annotated[str, typer.Option(prompt=True, show_default=False, help="Description of this flavor")],
#     ram: Annotated[int, typer.Option(prompt=True, show_default=False, help="The amount of RAM a flavor has, in MiB")],
#     vcpus: Annotated[
#         int,
#         typer.Option(
#             prompt=True, show_default=False, help="The number of virtual CPUs that will be allocated to the server"
#         ),
#     ],
#     disk: Annotated[
#         int,
#         typer.Option(prompt=True, show_default=False, help="The size of the root disk that will be created in MB."),
#     ],
# ):
#     """Create a flavor.
#
#     This flavor is stored in the project.
#     """
#     try:
#         flavor = (
#             ctx.obj.api.for_project(ctx.obj.require_project_id())
#             .bi(ctx.obj.infra_id)
#             .flavor.create_in_project(
#                 VMFlavorCreate(
#                     friendly_name=friendly_name,
#                     description=description,
#                     ram_mib=ram,
#                     vcpus=vcpus,
#                     disk_mb=disk,
#                 )
#             )
#         )
#         console.print(f"[bold green]:computer: Created {friendly_name} as {flavor.id} :white_check_mark:")
#     except BadRequestException as exc:
#         err_console.print("[bold red]Could not create flavor[/] :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 flavor.[/] :x:")
#         raise typer.Exit(code=1) from None
#     except ApiException as exc:
#         err_console.print("[bold red]Could not create flavor[/] :x:")
#         err_console.print(f"[red] {get_api_error_detail(exc)}")
#         raise typer.Exit(code=1) from None


class FlavorType(str, Enum):
    """The type of flavor."""

    vm = "vm"
    baremetal = "baremetal"


@flavors_app.command(name="create-global", rich_help_panel="Admin Commands")
def create_global_flavor(
    ctx: BIContext,
    friendly_name: Annotated[
        str,
        typer.Option(
            prompt=True,
            show_default=False,
            help='Short user readable name. On the current BIs, this should start with "custom."',
        ),
    ],
    description: Annotated[str, typer.Option(prompt=True, show_default=False, help="Description of this flavor")],
    type: Annotated[FlavorType, typer.Option(case_sensitive=True, help="The type of flavor.")],
    ram: Annotated[int, typer.Option(prompt=True, show_default=False, help="The amount of RAM a flavor has, in MiB")],
    disk: Annotated[
        int,
        typer.Option(prompt=True, show_default=False, help="The size of the root disk that will be created in MB."),
    ],
    vcpus: Annotated[
        int,
        typer.Option(
            prompt=True, show_default=False, help="The number of virtual CPUs that will be allocated to the server"
        ),
    ]
    | None = None,
    cpu_model: Annotated[
        str,
        typer.Option(prompt=True, show_default=False, help="The CPU model"),
    ]
    | None = None,
    cpu_sockets: Annotated[
        int,
        typer.Option(prompt=True, show_default=False, help="The number of Physical CPU sockets"),
    ]
    | None = None,
    cpu_cores: Annotated[
        int,
        typer.Option(prompt=True, show_default=False, help="The number of Physical CPU cores (per CPU socket)"),
    ]
    | None = None,
    cpu_threads: Annotated[
        int,
        typer.Option(prompt=True, show_default=False, help="The number of Physical CPU Threads (per CPU core)"),
    ]
    | None = None,
    gpu_model: Annotated[
        str,
        typer.Option(prompt=True, show_default=False, help="The GPU model (if a GPU is present)"),
    ]
    | None = None,
    network_interfaces: Annotated[
        int,
        typer.Option(prompt=True, show_default=False, help="The number of network interfaces"),
    ]
    | None = None,
):
    """Create a global flavor, available for everyone. Admin only."""
    try:
        if type == FlavorType.baremetal:
            if cpu_cores is None:
                err_console.print("[red] --cpu-cores required")
                raise typer.Exit(code=1) from None
            if cpu_model is None:
                err_console.print("[red] --cpu-model required")
                raise typer.Exit(code=1) from None
            if cpu_sockets is None:
                err_console.print("[red] --cpu-sockets required")
                raise typer.Exit(code=1) from None
            if cpu_threads is None:
                err_console.print("[red] --cpu-threads required")
                raise typer.Exit(code=1) from None
            flavor_create = BareMetalFlavorCreate(
                friendly_name=friendly_name,
                description=description,
                ram_mib=ram,
                disk_mb=disk,
                cpu_cores=cpu_cores,
                cpu_model=cpu_model,
                cpu_sockets=cpu_sockets,
                cpu_threads=cpu_threads,
                gpu_model=gpu_model,
                network_interface_count=network_interfaces or 0,
            )
        elif type == FlavorType.vm:
            if vcpus is None:
                err_console.print("[red] --vcpus required")
                raise typer.Exit(code=1) from None
            flavor_create = VMFlavorCreate(
                friendly_name=friendly_name,
                description=description,
                ram_mib=ram,
                vcpus=vcpus,
                disk_mb=disk,
            )
        else:
            raise ValueError(f"Unsupported flavor type {type}")

        flavor = (
            ctx.obj.api.for_project(ctx.obj.require_project_id())
            .bi(ctx.obj.infra_id)
            .flavor.create_global(flavor_create)
        )
        console.print(f"[bold green]:computer: Created {friendly_name} as {flavor.id} :white_check_mark:")
    except BadRequestException as exc:
        err_console.print("[bold red]Could not create flavor[/] :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 flavor.[/] :x:")
        raise typer.Exit(code=1) from None
    except ApiException as exc:
        err_console.print("[bold red]Could not create flavor[/] :x:")
        err_console.print(f"[red] {get_api_error_detail(exc)}")
        raise typer.Exit(code=1) from None


def _float_to_str(v: float) -> str:
    if float(round(v)) == v:
        return str(int(v))
    return str(round(v, 3))


def _extract_infa_id(any_type_id: str) -> str:
    return any_type_id.split("_")[1]


def _print_vm_flavors_table(infra: str, flavors: list, show_infra: bool, show_id: bool):
    names = ["Name", "Desc", "(v)CPUs", "RAM (GiB)", "Root Disk (GB)"]

    if show_infra:
        names.insert(0, "Infra")
    if show_id:
        names.insert(0, "ID")

    table = Table(
        *names,
        title=f"Slices BI Flavors on {infra}" if infra != "bi-orchestrator" else "Slices BI VM Flavors",
    )
    for flavor in flavors:
        if isinstance(flavor, VMFlavor):
            values = [
                flavor.friendly_name,
                flavor.description,
                str(flavor.vcpus),
                _float_to_str(flavor.ram_mib / 1024.0),
                _float_to_str(flavor.disk_mb / 1000.0),
            ]
            if show_infra:
                values.insert(0, _extract_infa_id(flavor.id))
            if show_id:
                values.insert(0, flavor.id)
            table.add_row(*values)

    console.print(table)


def _print_bare_metal_flavors_table(infra: str, flavors: list, show_infra: bool, show_id: bool):
    names = ["Name", "Desc", "CPUs", "RAM (GiB)", "Root Disk (GB)", "Interfaces", "GPU"]

    if show_infra:
        names.insert(0, "Infra")
    if show_id:
        names.insert(0, "ID")

    table = Table(
        *names,
        title=f"Slices BI Flavors on {infra}" if infra != "bi-orchestrator" else "Slices BI Baremetal Flavors",
    )
    for flavor in flavors:
        if isinstance(flavor, BareMetalFlavor):
            values = [
                flavor.friendly_name,
                flavor.description,
                f"{flavor.cpu_sockets}x{flavor.cpu_cores}x{flavor.cpu_threads}",
                _float_to_str(flavor.ram_mib / 1024.0),
                _float_to_str(flavor.disk_mb / 1000.0),
                str(flavor.network_interface_count),
                flavor.gpu_model if flavor.gpu_model else "-",
            ]
            if show_infra:
                values.insert(0, _extract_infa_id(flavor.id))
            if show_id:
                values.insert(0, flavor.id)
            table.add_row(*values)
    console.print(table)


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

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


def _flavor_sort_helper(flavor: AnyFlavor):
    if isinstance(flavor, BareMetalFlavor):
        return f"bm_{flavor.friendly_name}"
    if isinstance(flavor, VMFlavor):
        return f"vm_{flavor.vcpus:03d}_{flavor.ram_mib:07d}_{flavor.disk_mb:09d}_{flavor.friendly_name}"
    return f"xx_{flavor.friendly_name}"


@flavors_app.command(name="list", rich_help_panel="Commands")
@handle_some_exceptions
def list_flavors(
    ctx: BIContext,
    show_id: Annotated[
        bool,
        typer.Option(
            "--id/--no-id",
            "-i/-ni",
            help="Show/hide the internal ID of the flavor. (default: hide)",
            rich_help_panel="Table view column selection",
            show_default=False,
        ),
    ] = False,
    show_infra: Annotated[
        bool,
        typer.Option(
            "--infra/--no-infra",
            "-I/-nI",
            help="Show/hide the infrastructure ID of the flavor. (default: show)",
            rich_help_panel="Table view column selection",
            show_default=False,
        ),
    ] = True,
    format: Annotated[
        ListFormat,
        typer.Option("--format", "-f", help="Format in which the flavors should be listed."),
    ] = "table",  # type: ignore
):
    """List flavors."""
    flavors = ctx.obj.api.for_project(ctx.obj.require_project_id()).bi(ctx.obj.infra_id).flavor.list()

    # we need to run over the flavors twice, so we need to store them in a list
    flavors = list(flavors)
    flavors.sort(key=lambda f: (get_type_id_domain(f.id), _flavor_sort_helper(f)))

    if format == ListFormat.table:
        has_bare_metal = False
        has_vm = False

        for flavor in flavors:
            if isinstance(flavor, BareMetalFlavor):
                has_bare_metal = True
            elif isinstance(flavor, VMFlavor):
                has_vm = True

        infra = ctx.obj.infra_id

        if has_vm:
            _print_vm_flavors_table(flavors=flavors, infra=infra, show_infra=show_infra, show_id=show_id)
        if has_bare_metal:
            _print_bare_metal_flavors_table(flavors=flavors, infra=infra, show_infra=show_infra, show_id=show_id)

        # For reason of difference between MB and MiB for disk/ram, see https://en.wikipedia.org/wiki/Gigabyte
        # M/Mi instead of G/Gi everywhere to be flexible for IoT and other small devices

    elif format == ListFormat.json:
        console.print_json(json_encode(list(flavors)).decode())
    elif format == ListFormat.yaml:
        console.print(yaml_encode(list(flavors)).decode())
