"""Basic infrastructure CLI."""

import functools
import socket
from collections.abc import Collection

import typer
from slices_clientlib_bi.bi_resource import ComputeResource, ResourceState, SSHInfo
from slices_clientlib_core.base.exceptions import ApiResponseError, NotFoundException
from slices_clientlib_core.experiment import Experiment
from urllib3.connection import HTTPSConnection
from urllib3.exceptions import MaxRetryError, NewConnectionError

from slices_cli_bi import err_console
from slices_cli_bi.util.type_id import is_type_id

from .bi_context import BIContext


def bi_get_resource(ctx: BIContext, experiment: Experiment, resource_id: str) -> ComputeResource:
    """Get a resource for a bi command."""
    if is_type_id(resource_id, type="r"):
        try:
            return ctx.obj.get_bi_resources_api_by_experiment(experiment).get(resource_id)
        except NotFoundException as err:
            err_console.print(
                f"[red]Resource {resource_id} not found in experiment {experiment.friendly_name} "
                f"on infrastructure {ctx.obj.infra_id} :x:"
            )
            raise typer.Exit(code=1) from err

    resources = ctx.obj.get_bi_resources_api_by_experiment(experiment).list()

    inactive_resource = None

    def _is_inactive(resource: ComputeResource) -> bool:
        return resource.status in (ResourceState.DELETED, ResourceState.FAILED)

    for resource in resources:
        if resource.friendly_name.lower() == resource_id.lower():
            if _is_inactive(resource):
                if (
                    # no inactive resource with this name found yet
                    not inactive_resource
                    # more recent inactive resource has precedence
                    or resource.created_at > inactive_resource.created_at
                ):
                    inactive_resource = resource
            else:
                return resource

    # no active resource found, return inactive resource if found
    if inactive_resource:
        return inactive_resource

    err_console.print(
        f"[red]Resource {resource_id} not found in experiment {experiment.friendly_name} "
        f"on infrastructure {ctx.obj.infra_id} :x:"
    )
    raise typer.Exit(code=1)


def bi_get_requested_bi_resources(
    ctx: BIContext, exp: Experiment, resources: list[str] | None, include_expired: bool, include_terminated: bool
) -> Collection[ComputeResource]:
    """Get requested resources for a bi command."""
    if not resources:  # all resources
        return ctx.obj.get_bi_resources_api_by_experiment(exp).list(
            include_expired=include_expired,
            include_terminated=include_terminated,
        )

    return [bi_get_resource(ctx, exp, resource_id) for resource_id in resources]


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 ApiResponseError as e:
            if e._httpx_status_error.response.status_code in (
                502,
                503,
                504,
            ):
                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 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


def bi_get_experiment(ctx: BIContext, experiment_id: str) -> Experiment:
    """Get an experiment for a bi command."""
    if is_type_id(experiment_id, type="exp"):
        exp_obj = ctx.obj.api.experiment.get(experiment_id)
    else:
        exp_obj = ctx.obj.api.get_experiment(experiment_id, project_id=ctx.obj.require_project_id())

    if not exp_obj:
        err_console.print(f"Could not retrieve experiment {experiment_id} :x:")
        raise typer.Exit(code=1)
    return exp_obj


def _can_connect(host: str, port: int) -> bool:
    # TODO the timeout value is too short. Ideally, it should be based on known RTT of a slices service.

    try:
        with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
            s.settimeout(0.1)
            if s.connect_ex((host, port)) == 0:
                # if we can connect, return True
                return True
    except:  # noqa: E722 S110
        # ignore IPv6 problems
        pass

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(0.1)
            return s.connect_ex((host, port)) == 0
    except:  # noqa: E722
        # Any exception means we can't connect.
        # Examples:
        #      gaierror: [Errno -5] No address associated with hostname
        return False


def bi_get_best_ssh_login(compute_resource: ComputeResource, with_proxy: bool) -> tuple[SSHInfo | None, bool]:
    """Get the best SSH login for a resource."""
    if not compute_resource.ssh_logins:
        return None, False

    best_ssh_login = None
    best_ssh_login_can_connect = False
    best_score = -1

    # Prefer the first one that has a public key
    for ssh_login in compute_resource.ssh_logins:
        score = 0

        # User wants to use a proxy
        if with_proxy and ssh_login.jump_proxy:
            score += 1000

        # Prefer connecting to a public IP
        if compute_resource.public_ipv4 and compute_resource.public_ipv4 in ssh_login.host:
            score += 100

        # Prefer connecting without needing to use the proxy
        if can_connect := _can_connect(ssh_login.host, ssh_login.port):
            score += 10

        if score > best_score:
            best_ssh_login = ssh_login
            best_ssh_login_can_connect = can_connect
            best_score = score

    # Otherwise, just return the first one
    return best_ssh_login, best_ssh_login_can_connect
