import argparse
import json
import logging
import os
import shlex
import sys

from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..facts.generate import generate_facts
from ..facts.upload import upload_secrets
from ..machines.machines import Machine
from ..nix import nix_command, nix_metadata
from ..ssh import HostKeyCheck
from .inventory import get_all_machines, get_selected_machines
from .machine_group import MachineGroup

log = logging.getLogger(__name__)


def is_path_input(node: dict[str, dict[str, str]]) -> bool:
    locked = node.get("locked")
    if not locked:
        return False
    return locked["type"] == "path" or locked.get("url", "").startswith("file://")


def upload_sources(
    flake_url: str, remote_url: str, always_upload_source: bool = False
) -> str:
    if not always_upload_source:
        flake_data = nix_metadata(flake_url)
        url = flake_data["resolvedUrl"]
        has_path_inputs = any(
            is_path_input(node) for node in flake_data["locks"]["nodes"].values()
        )
        if not has_path_inputs and not is_path_input(flake_data):
            # No need to upload sources, we can just build the flake url directly
            # FIXME: this might fail for private repositories?
            return url
        if not has_path_inputs:
            # Just copy the flake to the remote machine, we can substitute other inputs there.
            path = flake_data["path"]
            env = os.environ.copy()
            # env["NIX_SSHOPTS"] = " ".join(opts.remote_ssh_options)
            assert remote_url
            cmd = nix_command(
                [
                    "copy",
                    "--to",
                    f"ssh://{remote_url}",
                    "--no-check-sigs",
                    path,
                ]
            )
            run(cmd, env=env, error_msg="failed to upload sources")
            return path

    # Slow path: we need to upload all sources to the remote machine
    assert remote_url
    cmd = nix_command(
        [
            "flake",
            "archive",
            "--to",
            f"ssh://{remote_url}",
            "--json",
            flake_url,
        ]
    )
    log.info("run %s", shlex.join(cmd))
    proc = run(cmd, error_msg="failed to upload sources")

    try:
        return json.loads(proc.stdout)["path"]
    except (json.JSONDecodeError, OSError) as e:
        raise ClanError(
            f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout}"
        )


def deploy_machine(machines: MachineGroup) -> None:
    """
    Deploy to all hosts in parallel
    """

    def deploy(machine: Machine) -> None:
        host = machine.build_host
        target = f"{host.user or 'root'}@{host.host}"
        ssh_arg = f"-p {host.port}" if host.port else ""
        env = os.environ.copy()
        env["NIX_SSHOPTS"] = ssh_arg

        generate_facts([machine], None, False)
        upload_secrets(machine)

        path = upload_sources(".", target)

        if host.host_key_check != HostKeyCheck.STRICT:
            ssh_arg += " -o StrictHostKeyChecking=no"
        if host.host_key_check == HostKeyCheck.NONE:
            ssh_arg += " -o UserKnownHostsFile=/dev/null"

        ssh_arg += " -i " + host.key if host.key else ""

        cmd = [
            "nixos-rebuild",
            "switch",
            "--fast",
            "--option",
            "keep-going",
            "true",
            "--option",
            "accept-flake-config",
            "true",
            "--build-host",
            "",
            *machine.nix_options,
            "--flake",
            f"{path}#{machine.name}",
        ]
        if target_host := host.meta.get("target_host"):
            target_host = f"{target_host.user or 'root'}@{target_host.host}"
            cmd.extend(["--target-host", target_host])
        ret = host.run(cmd, check=False)
        # re-retry switch if the first time fails
        if ret.returncode != 0:
            ret = host.run(cmd)

    machines.run_function(deploy)


def update(args: argparse.Namespace) -> None:
    if args.flake is None:
        raise ClanError("Could not find clan flake toplevel directory")
    machines = []
    if len(args.machines) == 1 and args.target_host is not None:
        machine = Machine(
            name=args.machines[0], flake=args.flake, nix_options=args.option
        )
        machine.target_host_address = args.target_host
        machines.append(machine)

    elif args.target_host is not None:
        print("target host can only be specified for a single machine")
        exit(1)
    else:
        if len(args.machines) == 0:
            ignored_machines = []
            for machine in get_all_machines(args.flake, args.option):
                if machine.deployment.get("requireExplicitUpdate", False):
                    continue
                try:
                    machine.build_host
                except ClanError:  # check if we have a build host set
                    ignored_machines.append(machine)
                    continue

                machines.append(machine)

            if not machines and ignored_machines != []:
                print(
                    "WARNING: No machines to update. The following defined machines were ignored because they do not have `clan.core.networking.targetHost` nixos option set:",
                    file=sys.stderr,
                )
                for machine in ignored_machines:
                    print(machine, file=sys.stderr)

        else:
            machines = get_selected_machines(args.flake, args.option, args.machines)

    deploy_machine(MachineGroup(machines))


def register_update_parser(parser: argparse.ArgumentParser) -> None:
    machines_parser = parser.add_argument(
        "machines",
        type=str,
        nargs="*",
        default=[],
        metavar="MACHINE",
        help="machine to update. If no machine is specified, all machines will be updated.",
    )

    add_dynamic_completer(machines_parser, complete_machines)

    parser.add_argument(
        "--target-host",
        type=str,
        help="address of the machine to update, in the format of user@host:1234",
    )
    parser.add_argument(
        "--darwin",
        type=str,
        help="Hack to deploy darwin machines. This will be removed in the future when we have full darwin integration.",
    )
    parser.set_defaults(func=update)
