"""BI Machines CLI."""

import functools
from collections import defaultdict
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.base.exceptions import InfraFunctionalityNotImplemented
from slices_clientlib_bi.bi_machine import AnyLocation, GeoLocation, GridLocation, Machine, RackAndPosition
from slices_clientlib_core.base.exceptions import NotFoundException, 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.type_id import is_type_id
from slices_cli_bi.util.typer import AliasGroup

from .bi_context import BIContext


class MachineAliasGroup(AliasGroup):
    """AliasGroup hack for slices machine command.

    By using this subclass, an independent version of the _hidden_aliases list is used.
    """


MachineAliasGroup.register_hidden_alias("count", "counts")
machines_app = typer.Typer(no_args_is_help=True, short_help="Slices BI Machines.", cls=MachineAliasGroup)


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: {e}")
            raise typer.Exit(code=1) from e
        except InfraFunctionalityNotImplemented:
            err_console.print("[red]The machine functionality is not implemented at the infrastructure. :x:")
            raise typer.Exit(code=1) from None
        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


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

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


def _location_str(location: AnyLocation) -> str:
    if location is None:
        return ""
    if isinstance(location, GeoLocation):
        return f"{location.country} ({location.lat}, {location.long})"
    if isinstance(location, GridLocation):
        return f"({location.x}, {location.y}, {location.z})"
    if isinstance(location, RackAndPosition):
        return f"{location.rack} {location.position}"
    return f"unknown ({type(location)})"


class EnrichedMachine(Machine):
    """Machine info with flavor names added."""

    flavor_names: list[str] = []


def enrich_machines(ctx: BIContext, machines: list[Machine]) -> list[EnrichedMachine]:
    """Add the names of the flavors to each machine ina  list of machines."""
    flavors = list(ctx.obj.api.for_project(ctx.obj.require_project_id()).bi(ctx.obj.infra_id).flavor.list())

    def _flavor_name(flavor_id: str) -> str:
        for flavor in flavors:
            if flavor.id == flavor_id:
                return flavor.friendly_name
        return "unknown"

    return [
        EnrichedMachine(
            id=m.id,
            name=m.name,
            description=m.description,
            available=m.available,
            tags=m.tags,
            flavors=m.flavors,
            location=m.location,
            flavor_names=[_flavor_name(f) for f in m.flavors],
        )
        for m in machines
    ]


class ListMachineSort(Enum):
    """List sort options."""

    name = "name"  # pyright: ignore [reportIncompatibleMethodOverride,reportAssignmentType] seems to work fine
    available = "available"


@machines_app.command(name="list", rich_help_panel="Commands")
@handle_some_exceptions
def list_machines(
    ctx: BIContext,
    format: Annotated[
        ListFormat,
        typer.Option("--format", "-f", help="Format in which the machines should be listed."),
    ] = "table",  # type: ignore
    include_unavailable: Annotated[
        bool,
        typer.Option(
            "--show-unavailable/--hide-unavailable",
            "-su/-hu",
            help="Show/Hide unavailable machines.",
        ),
    ] = False,
    sort: Annotated[ListMachineSort, typer.Option("--sort", "-s", help="Primary field to sort by.")] = "name",  # type: ignore
):
    """List Slices BI Machines."""
    machines = list(ctx.obj.api.for_project(ctx.obj.require_project_id()).bi(ctx.obj.infra_id).machine.list())

    if sort == ListMachineSort.name:
        machines.sort(key=lambda m: (m.name, not (m.available.available if m.available else False)))
    elif sort == ListMachineSort.available:
        machines.sort(key=lambda m: (not (m.available.available if m.available else False), m.name))

    if not include_unavailable:
        machines = [m for m in machines if m.available.available]

    # enrich machine
    machines = enrich_machines(ctx, machines)

    if format == ListFormat.table:
        has_geo = False
        has_grid = False
        has_rack = False
        for m in machines:
            for loc in m.location:
                if isinstance(loc, GeoLocation):
                    has_geo = True
                elif isinstance(loc, GridLocation):
                    has_grid = True
                elif isinstance(loc, RackAndPosition):
                    has_rack = True

        cols = ["ID", "Name", "Available", "Description", "Flavors", "Tags"]
        if has_geo:
            cols.append("Geo Location (lat,long)")
        if has_grid:
            cols.append("Grid Location")
        if has_rack:
            cols.append("Rack Location")

        table = Table(*cols, title="All Machines" if include_unavailable else "Available Machines")

        for machine in machines:
            values = [
                machine.id,
                machine.name,
                str(machine.available.available),
                machine.description,
                ",".join(machine.flavor_names),
                ",".join(machine.tags),
            ]
            if has_geo:
                if has_geo:
                    geo_loc = ",".join([_location_str(loc) for loc in machine.location if isinstance(loc, GeoLocation)])
                    values.append(geo_loc)
                if has_grid:
                    grid_loc = ",".join(
                        [_location_str(loc) for loc in machine.location if isinstance(loc, GridLocation)]
                    )
                    values.append(grid_loc)
                if has_rack:
                    rack_loc = ",".join(
                        [_location_str(loc) for loc in machine.location if isinstance(loc, RackAndPosition)]
                    )
                    values.append(rack_loc)

            table.add_row(*values)

        console.print(table)

    elif format == ListFormat.json:
        console.print_json(json_encode(machines).decode())
    elif format == ListFormat.yaml:
        console.print("---")
        console.print(yaml_encode(machines).decode())


class ShowMachineFormat(Enum):
    """Show Machine Formats."""

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


@machines_app.command(rich_help_panel="Commands")
@handle_some_exceptions
def show(
    ctx: BIContext,
    machine_id: Annotated[str, typer.Argument(help="Machine ID or name.")],
    format: Annotated[ShowMachineFormat, typer.Option("--format", "-f", help="Output format.")] = "table",  # type: ignore
):
    """Show info about a specific machine."""

    if is_type_id(machine_id):
        try:
            machine_info = (
                ctx.obj.api.for_project(ctx.obj.require_project_id()).bi(ctx.obj.infra_id).machine.get(machine_id)
            )
        except NotFoundException:
            err_console.print(f"[red]Did not find machine with ID {machine_id!r} :x:")
            raise typer.Exit(1) from None
    else:
        machines = list(ctx.obj.api.for_project(ctx.obj.require_project_id()).bi(ctx.obj.infra_id).machine.list())
        matching_machines = [m for m in machines if m.name == machine_id]
        if not matching_machines:
            err_console.print(f"[red]Did not find machine with name {machine_id!r} :x:")
            raise typer.Exit(1)
        if len(matching_machines) > 1:
            err_console.print(
                f"[red] :x: Found multiple machines with name {machine_id!r}: "
                f"{', '.join([m.id for m in matching_machines])}"
            )
            raise typer.Exit(1)
        machine_info = matching_machines[0]

    # enrich machine
    machine_info = enrich_machines(ctx, [machine_info])[0]

    if format == ShowMachineFormat.table:
        has_geo = False
        has_grid = False
        has_rack = False
        for loc in machine_info.location:
            if isinstance(loc, GeoLocation):
                has_geo = True
            elif isinstance(loc, GridLocation):
                has_grid = True
            elif isinstance(loc, RackAndPosition):
                has_rack = True

        cols = ["ID", "Name", "Available", "Description", "Flavors", "Tags"]
        if has_geo:
            cols.append("Geo Location (lat,long)")
        if has_grid:
            cols.append("Grid Location")
        if has_rack:
            cols.append("Rack Location")

        table = Table(*cols, title="Machines")
        values = [
            machine_info.id,
            machine_info.name,
            str(machine_info.available.available),
            machine_info.description,
            ",".join(machine_info.flavor_names),
            ",".join(machine_info.tags),
        ]
        if has_geo:
            if has_geo:
                geo_loc = ",".join(
                    [_location_str(loc) for loc in machine_info.location if isinstance(loc, GeoLocation)]
                )
                values.append(geo_loc)
            if has_grid:
                grid_loc = ",".join(
                    [_location_str(loc) for loc in machine_info.location if isinstance(loc, GridLocation)]
                )
                values.append(grid_loc)
            if has_rack:
                rack_loc = ",".join(
                    [_location_str(loc) for loc in machine_info.location if isinstance(loc, RackAndPosition)]
                )
                values.append(rack_loc)
        table.add_row(*values)
        console.print(table)
    elif format == ShowMachineFormat.json:
        console.print_json(json_encode(machine_info).decode())
    elif format == ShowMachineFormat.yaml:
        console.print("---")
        console.print(yaml_encode(machine_info).decode())


@machines_app.command(name="count", rich_help_panel="Commands")
@handle_some_exceptions
def machine_counts(
    ctx: BIContext,
    format: Annotated[
        ListFormat,
        typer.Option("--format", "-f", help="Format in which the machines should be listed."),
    ] = "table",  # type: ignore
):
    """List Machines Statistics (machine count per flavor type)."""
    machines = list(ctx.obj.api.for_project(ctx.obj.require_project_id()).bi(ctx.obj.infra_id).machine.list())

    # enrich machines
    machines = enrich_machines(ctx, machines)

    # multi_flavor = False
    # no_flavor = False

    t_counts = len(machines)
    t_avail_counts = 0
    f_counts = defaultdict(int)
    f_avail_counts = defaultdict(int)
    for machine in machines:
        if machine.available.available:
            t_avail_counts += 1
        # if len(machine.flavor_names) > 1:
        #     multi_flavor = True
        # if len(machine.flavor_names) == 0:
        #     no_flavor = True
        for flavor_name in machine.flavor_names:
            f_counts[flavor_name] += 1
            if machine.available.available:
                f_avail_counts[flavor_name] += 1

    by_flavor_obj = {}
    obj = {
        "total_count": t_counts,
        "available_count": t_avail_counts,
        "unavailable_count": t_counts - t_avail_counts,
        "by_flavor": by_flavor_obj,
    }
    for flavor_name, cur_f_count in f_counts.items():
        cur_f_avail_count = f_avail_counts[flavor_name]
        by_flavor_obj[flavor_name] = {
            "total_count": cur_f_count,
            "available_count": cur_f_avail_count,
            "unavailable_count": cur_f_count - cur_f_avail_count,
        }

    if format == ListFormat.table:
        cols = ["Flavor", "Available", "Unavailable", "Total"]

        table = Table(*cols, title="Machines")

        for flavor_name, f_counts in by_flavor_obj.items():
            values = [
                flavor_name,
                str(f_counts["available_count"]),
                str(f_counts["unavailable_count"]),
                str(f_counts["total_count"]),
            ]

            table.add_row(*values)

        # table.add_section()
        # table.add_row("ANY", str(t_avail_counts), str(t_counts - t_avail_counts), str(t_counts), style="bold")
        console.print(table)
        # if multi_flavor or no_flavor:
        #     console.print(
        #         "[grey]Note: Some machines have multiple flavors or no flavors at all.\n"
        #         '      Because of this "[bold]ANY[/bold]" is not equal to the sum of the numbers above.',
        #         highlight=False,
        #     )

    elif format == ListFormat.json:
        console.print_json(json_encode(obj).decode())
    elif format == ListFormat.yaml:
        console.print("---")
        console.print(yaml_encode(obj).decode())
