import json
import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any, Literal

from clan_cli.facts.generate import generate_facts
from clan_cli.vars.generator import Generator
from clan_cli.vars.upload import populate_secret_vars

from clan_lib.api import API
from clan_lib.cmd import Log, RunOpts, cmd_with_root, run
from clan_lib.dirs import clan_core_flake
from clan_lib.errors import ClanError
from clan_lib.flake.flake import Flake
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_shell
from clan_lib.vars.generate import run_generators

from .automount import pause_automounting
from .list import list_keymaps, list_languages

log = logging.getLogger(__name__)


@dataclass
class SystemConfig:
    keymap: str = field(default="en")
    language: str = field(
        default="en_US.UTF-8",
    )  # Leave this default, or implement virtual scrolling for the 400+ options in the UI.
    ssh_keys_path: list[str] | None = field(default=None)


@dataclass
class Disk:
    name: str
    device: str


installer_machine = Machine(name="flash-installer", flake=Flake(str(clan_core_flake())))


def build_system_config_nix(system_config: SystemConfig) -> dict[str, Any]:
    """Translate ``SystemConfig`` to the structure expected by disko-install."""
    system_config_nix: dict[str, Any] = {}

    if system_config.language:
        languages = list_languages()
        if system_config.language not in languages:
            msg = (
                f"Language '{system_config.language}' is not a valid language. "
                "Run 'clan flash list languages' to see a list of possible languages."
            )
            raise ClanError(msg)
        system_config_nix["i18n"] = {"defaultLocale": system_config.language}

    if system_config.keymap:
        keymaps = list_keymaps()
        if system_config.keymap not in keymaps:
            msg = (
                f"Keymap '{system_config.keymap}' is not a valid keymap. "
                "Run 'clan flash list keymaps' to see a list of possible keymaps."
            )
            raise ClanError(msg)
        system_config_nix["console"] = {"keyMap": system_config.keymap}
        system_config_nix["services"] = {
            "xserver": {"xkb": {"layout": system_config.keymap}},
        }

    if system_config.ssh_keys_path:
        root_keys = []
        for key_path in (Path(x) for x in system_config.ssh_keys_path):
            try:
                root_keys.append(key_path.read_text())
            except OSError as e:
                msg = f"Cannot read SSH public key file: {key_path}: {e}"
                raise ClanError(msg) from e
        system_config_nix["users"] = {
            "users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}},
        }

    return system_config_nix


# TODO: unify this with machine install
@API.register
def run_machine_flash(
    disks: list[Disk],
    system_config: SystemConfig,
    # Optional parameters
    machine: Machine = installer_machine,
    mode: Literal["format", "mount"] = "format",
    dry_run: bool = False,
    write_efi_boot_entries: bool = False,
    debug: bool = False,
    extra_args: list[str] | None = None,
    graphical: bool = False,
) -> None:
    """Flash a machine with the given configuration.

    Args:
        machine: The Machine instance to flash.
        mode: The mode to use for flashing (e.g., "install", "reinstall
        disks: List of Disk instances representing the disks to flash.
        system_config: SystemConfig instance containing language, keymap, and SSH keys.
        dry_run: If True, perform a dry run without making changes.
        write_efi_boot_entries: If True, write EFI boot entries.
        debug: If True, enable debug mode.
        extra_args: Additional arguments to pass to the disko-install command.
        graphical: If True, run the command in graphical mode.

    Raises:
        ClanError: If the language or keymap is invalid, or if there are issues with
            reading SSH keys, or if disko-install fails.

    """
    devices = [Path(disk.device) for disk in disks]
    with pause_automounting(devices, machine, request_graphical=graphical):
        if extra_args is None:
            extra_args = []

        generate_facts([machine])
        run_generators([machine], generators=None, full_closure=False)

        system_config_nix = build_system_config_nix(system_config)

        for generator in Generator.get_machine_generators(
            [machine.name], machine.flake
        ):
            for file in generator.files:
                if file.needed_for == "partitioning":
                    msg = f"Partitioning time secrets are not supported with `clan flash write`: clan.core.vars.generators.{generator.name}.files.{file.name}"
                    raise ClanError(msg)

        with TemporaryDirectory(prefix="disko-install-") as _tmpdir:
            tmpdir = Path(_tmpdir)
            upload_dir = machine.secrets_upload_directory

            if upload_dir.startswith("/"):
                local_dir = tmpdir / upload_dir[1:]
            else:
                local_dir = tmpdir / upload_dir

            local_dir.mkdir(parents=True)
            machine.secret_facts_store.upload(local_dir)
            populate_secret_vars(machine, local_dir)
            disko_install = []

            if os.geteuid() != 0:
                wrapper = " ".join(
                    [
                        "disko_install=$(command -v disko-install);",
                        "exec",
                        *cmd_with_root(['"$disko_install" "$@"'], graphical=graphical),
                    ],
                )
                disko_install.extend(["bash", "-c", wrapper])

            disko_install.append("disko-install")
            if write_efi_boot_entries:
                disko_install.append("--write-efi-boot-entries")
            if dry_run:
                disko_install.append("--dry-run")
            if debug:
                disko_install.append("--debug")
            for disk in disks:
                disko_install.extend(["--disk", disk.name, disk.device])

                log.info("Will flash disk %s: %s", disk.name, disk.device)

            disko_install.extend(["--extra-files", str(local_dir), upload_dir])
            disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
            disko_install.extend(["--mode", str(mode)])
            disko_install.extend(
                [
                    "--system-config",
                    json.dumps(system_config_nix),
                ],
            )
            disko_install.extend(["--option", "dry-run", "true"])
            disko_install.extend(extra_args)

            cmd = nix_shell(
                ["disko"],
                disko_install,
            )
            run(
                cmd,
                RunOpts(
                    log=Log.BOTH,
                    error_msg=f"Failed to flash {machine}",
                    needs_user_terminal=True,
                ),
            )
