"""Find all installed subcommand CLIs."""

import json
import os
import shutil
import sys
from dataclasses import dataclass, field
from importlib.metadata import version as metadata_version
from json import JSONDecodeError
from pathlib import Path
from subprocess import TimeoutExpired, run

from . import console_trace, err_console
from .cache import (
    get_cached_subcommand_info,
    get_cached_subcommand_manifest,
    save_cached_subcommand_info,
    save_cached_subcommand_manifest,
)
from .get_version import get_executable_version
from .manifest import Manifest, ManifestException, process_manifest


@dataclass
class SubCommand:
    """Info about a subcommand CLI."""

    name: str

    executable: Path
    "path of the executable"

    is_ok: bool
    "whether the subcommand is working correctly"

    is_core: bool = False
    "whether the subcommand is a part of slices-cli-core"

    visible_aliases: list[str] = field(default_factory=list)
    "aliases for the subcommand, shown in --help"

    hidden_aliases: list[str] = field(default_factory=list)
    "aliases for the subcommand, not shown in --help or tab completion"

    tab_completion_supported: bool = False
    "whether the subcommand supports command line tab completion"

    tab_completion_install_option: str | list[str] | None = None
    "option to use to install command line tab completion."

    tab_completion_show_option: str | list[str] | None = None
    "option to use to show command line tab completion script."

    short_help: str = "DEFAULT"
    "help oneliner shown when main CLI --help is used"

    cached_subcommand_version: str | None = None
    "version of the subcommand as returned by --version"


def _report_subcommand_manifest_problem(executable: Path, e: Exception):
    """Report a problem with the subcommand manifest."""
    err_console.print(f"[red bold] :x: Error parsing subcommand manifest from {executable}", highlight=False)
    err_console.print(f"[red bold] {getattr(e, 'message', repr(e))}", highlight=False)


def _get_subcommand_manifest(
    executable: Path, *, require_subcommand_with_exe_name: bool = True
) -> tuple[dict | None, Manifest | None]:
    # print(f"_get_subcommand_manifest {executable}")
    try:
        console_trace.print("[bright_black]  executing %s --cli-manifest", executable.absolute())
        manifest_res = run([str(executable.absolute()), "--cli-manifest"], check=False, capture_output=True, timeout=15)  # noqa: S603
        console_trace.print("[bright_black]  returncode: %s", manifest_res.returncode)
        if manifest_res.returncode != 0:
            err_console.print(
                f"[red bold] :x: Error parsing subcommand manifest from {executable} (command failed)", highlight=False
            )
            console_trace.print("[bright_black]  stdout: %s", manifest_res.stdout.decode("utf-8"))
            console_trace.print("[bright_black]  stderr: %s", manifest_res.stderr.decode("utf-8"))
            return None, None

        manifest_data = json.loads(manifest_res.stdout.decode("utf-8"))
        console_trace.print("[bright_black]  loaded manifest data: %s", json.dumps(manifest_data, indent=2))
        return manifest_data, process_manifest(
            executable.name, manifest_data, require_subcommand_with_exe_name=require_subcommand_with_exe_name
        )
    except JSONDecodeError as e:
        _report_subcommand_manifest_problem(executable, e)
        return None, None
    except TimeoutExpired as e:
        _report_subcommand_manifest_problem(executable, e)
        return None, None
    except ManifestException as e:
        _report_subcommand_manifest_problem(executable, e)
        return None, None


# TODO cache subcommand manifest scan
#      use subcommand executable modification time to known when cached manifest is stale


def find_core_subcommands() -> list[SubCommand]:
    """Find slices-cli-core subcommand CLIs."""
    slices_core_which = shutil.which("slices-core")
    if not slices_core_which:
        err_console.print("[red bold] :x: Slices CLI core is not installed.", highlight=False)
        sys.exit(2)

    slices_core_path = Path(slices_core_which)

    if not slices_core_path.is_file() or not os.access(slices_core_path, os.X_OK):
        err_console.print("[red bold] :x: Slices CLI core is incorrectly installed.", highlight=False)
        sys.exit(2)

    # core_version = get_executable_version(slices_core_path)
    core_version = f"v{metadata_version('slices-cli-core')}"
    console_trace.print(f"[bright_black]core_version={core_version}")

    console_trace.print(f"[bright_black]checking core executable: '{slices_core_path}'")

    cached_manifest = get_cached_subcommand_manifest(slices_core_path, "core")
    if not cached_manifest:
        console_trace.print("[bright_black]  no cached core manifest")
        manifest_dict, manifest = _get_subcommand_manifest(slices_core_path, require_subcommand_with_exe_name=False)
        if manifest_dict and manifest:
            console_trace.print("[bright_black]  save core manifest to cache")
            save_cached_subcommand_manifest(slices_core_path, "core", manifest_dict)
        else:
            err_console.print("[red bold] :x: Slices CLI core is incorrectly installed.", highlight=False)
            sys.exit(2)
    else:
        console_trace.print("[bright_black]  using cached core manifest")
        manifest = process_manifest(slices_core_path.name, cached_manifest, require_subcommand_with_exe_name=False)

    if manifest is None:
        err_console.print("[red bold] :x: Slices CLI core is incorrectly installed.", highlight=False)
        sys.exit(2)

    return [
        SubCommand(
            name=subcommand.name,
            executable=slices_core_path,
            tab_completion_supported=manifest.tab_completion_supported,
            tab_completion_install_option=manifest.tab_completion_install_option,
            tab_completion_show_option=manifest.tab_completion_show_option,
            short_help=subcommand.short_help,
            is_ok=True,
            is_core=True,
            cached_subcommand_version=core_version,
            visible_aliases=subcommand.visible_aliases,
            hidden_aliases=subcommand.hidden_aliases,
        )
        for subcommand in manifest.subcommands
    ]

    # fallback
    return [
        SubCommand(
            name="auth",
            executable=slices_core_path,
            tab_completion_supported=True,
            tab_completion_install_option="--install-completion",
            short_help="User authentication",
            is_ok=True,
            is_core=True,
            cached_subcommand_version=core_version,
        ),
        SubCommand(
            name="project",
            executable=slices_core_path,
            tab_completion_supported=True,
            tab_completion_install_option="--install-completion",
            short_help="Projects",
            is_ok=True,
            is_core=True,
            cached_subcommand_version=core_version,
        ),
        SubCommand(
            name="experiment",
            executable=slices_core_path,
            tab_completion_supported=True,
            tab_completion_install_option="--install-completion",
            short_help="Experiments",
            is_ok=True,
            is_core=True,
            cached_subcommand_version=core_version,
        ),
        SubCommand(
            name="pubkey",
            executable=slices_core_path,
            tab_completion_supported=True,
            tab_completion_install_option="--install-completion",
            short_help="SSH key management",
            is_ok=True,
            is_core=True,
            cached_subcommand_version=core_version,
        ),
    ]


# based on from shutil.py
def _get_path() -> list[str] | None:
    path = os.environ.get("PATH", None)
    if path is None:
        try:
            path = os.confstr("CS_PATH")
        except (AttributeError, ValueError):
            # os.confstr() or CS_PATH is not available
            path = os.defpath
    # bpo-35755: Don't use os.defpath if the PATH environment variable
    # is set to an empty string

    # PATH='' doesn't match, whereas PATH=':' looks in the current
    # directory
    if not path:
        return None

    path = os.fsdecode(path)
    return path.split(os.pathsep)


def warn_if_completion_not_installed(subcommands: list[SubCommand]):
    """Warn if no completion is installed for a new subcommand."""

    main_completions_installed = (Path.home() / ".bash_completions" / "slices.sh").is_file() or (
        Path.home() / ".zfunc/_slices"
    ).is_file()
    if not main_completions_installed:
        return

    for subcommand in subcommands:
        if subcommand.is_core:
            continue
        is_installed = get_cached_subcommand_info(
            subcommand.executable, subcommand.name, "completion_installed", invalidate_on_exe_update=False
        )
        if is_installed != "true" and subcommand.tab_completion_supported:
            err_console.print(f"[yellow] :warning: No completion is installed for 'slices-{subcommand.name}'.")
            err_console.print("[yellow] :warning: You need rerun 'slices completion install' to install")
            err_console.print()


def find_subcommands() -> list[SubCommand]:
    """Find all installed subcommand CLIs."""
    core_subcommands = find_core_subcommands()

    known_executables = {"slices-core"}

    res = []

    path = _get_path()
    console_trace.print(f"[bright_black]PATH={path}")
    if not path:
        return core_subcommands

    # find all executables matching slices-*
    for d in path:
        for executable in Path(d).glob("slices-*"):
            if executable.is_file() and os.access(executable, os.X_OK) and executable.name not in known_executables:
                console_trace.print(f"[bright_black]found slices-cli executable: '{executable}'")
                known_executables.add(executable.name)
                name = str(executable.name[len("slices-") :])
                console_trace.print(f"[bright_black]  name={name}")

                cached_manifest = get_cached_subcommand_manifest(executable, name)
                if not cached_manifest:
                    console_trace.print("[bright_black]  no cached manifest")
                    manifest_dict, manifest = _get_subcommand_manifest(executable)
                    if manifest_dict and manifest:
                        console_trace.print("[bright_black]  save manifest to cache")
                        save_cached_subcommand_manifest(executable, name, manifest_dict)
                    else:
                        # save cache failure
                        fake_manifest = {
                            "@type": "slices-cli:subcommand-metadata",
                            "version": "1.0",
                            "tab_completion_supported": False,
                            "short_help": "WORKING INCORRECTLY",
                        }
                        console_trace.print("[bright_black]  save failure to cache")
                        save_cached_subcommand_manifest(executable, name, fake_manifest)
                else:
                    console_trace.print("[bright_black]  using cached manifest")
                    manifest = process_manifest(executable.name, cached_manifest)

                subcommand_version = get_cached_subcommand_info(
                    executable, name, "cli_version", invalidate_on_exe_update=True
                )
                if not subcommand_version:
                    subcommand_version = get_executable_version(executable)
                    save_cached_subcommand_info(executable, name, "cli_version", subcommand_version)

                if manifest is not None:
                    for subcommand in manifest.subcommands:
                        res.append(  # noqa: PERF401
                            SubCommand(
                                name=subcommand.name,
                                executable=executable,
                                tab_completion_supported=manifest.tab_completion_supported,
                                tab_completion_install_option=manifest.tab_completion_install_option,
                                tab_completion_show_option=manifest.tab_completion_show_option,
                                short_help=subcommand.short_help,
                                is_ok=True,
                                cached_subcommand_version=subcommand_version,
                                visible_aliases=subcommand.visible_aliases,
                                hidden_aliases=subcommand.hidden_aliases,
                            )
                        )
                else:
                    res.append(
                        SubCommand(
                            name=name,
                            executable=executable,
                            short_help="WORKING INCORRECTLY",
                            is_ok=False,
                            cached_subcommand_version=subcommand_version,
                        )
                    )

    return core_subcommands + res


def find_subcommand(name: str) -> SubCommand | None:
    """Find a subcommand CLI by name."""
    all = find_subcommands()
    warn_if_completion_not_installed(all)
    for subcommand in all:
        if subcommand.name == name or name in subcommand.visible_aliases or name in subcommand.hidden_aliases:
            return subcommand
    return None
