#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat >&2 <<'EOF'
Usage:
  ./install.sh
  ./install.sh [--repo <git-url>] interactive
  ./install.sh [--repo <git-url>] check <host>
  ./install.sh [--repo <git-url>] key-check <host>
  ./install.sh [--repo <git-url>] preflight [--generated-disko] <host> [target-host]
  ./install.sh remote [--generated-disko] <host> <target-host> [nixos-anywhere args...]
  ./install.sh local  <host> <mountpoint>

Examples:
  curl -L https://nix.bresilla.dev | bash
  curl -L https://nix.bresilla.dev | bash -s -- interactive
  curl -L https://raw.githubusercontent.com/bresilla/nixos/refs/heads/main/install.sh | bash -s -- check core
  curl -L https://raw.githubusercontent.com/bresilla/nixos/refs/heads/main/install.sh | bash -s -- key-check core
  curl -L https://raw.githubusercontent.com/bresilla/nixos/refs/heads/main/install.sh | bash -s -- preflight core nixos@192.168.100.163
  curl -L https://raw.githubusercontent.com/bresilla/nixos/refs/heads/main/install.sh | bash -s -- remote core nixos@192.168.100.163
  ./install.sh preflight --generated-disko core nixos@192.168.100.163
  ./install.sh remote --generated-disko core nixos@192.168.100.163
  ./install.sh check core
  ./install.sh key-check core
  ./install.sh preflight core nixos@192.168.100.163
  ./install.sh remote core nixos@192.168.100.163
  ./install.sh local core /mnt
EOF
}

die() {
  echo "error: $*" >&2
  exit 1
}

default_repo="https://github.com/bresilla/nixos.git"
repo_url="${NIXOS_INSTALL_REPO:-$default_repo}"

if [[ "${1:-}" == "--repo" ]]; then
  repo_url="${2:-}"
  [[ -n "$repo_url" ]] || die "--repo requires a git URL"
  shift 2
fi

mode="${1:-}"
host="${2:-}"

script_source="${BASH_SOURCE[0]-}"
if [[ -n "$script_source" && "$script_source" != "bash" && "$script_source" != "-" ]]; then
  repo_dir="$(cd -- "$(dirname -- "$script_source")" && pwd)"
else
  repo_dir="$(pwd)"
fi

if [[ ! -f "$repo_dir/flake.nix" || ! -f "$repo_dir/.sops.yaml" || ! -d "$repo_dir/secrets/host-keys" ]]; then
  command -v git >/dev/null || die "git is not in PATH and the script is not running from a repo checkout"

  checkout_dir="${NIXOS_INSTALL_DIR:-$HOME/nixos_install}"

  if [[ -d "$checkout_dir/.git" ]]; then
    if git -C "$checkout_dir" diff --quiet && git -C "$checkout_dir" diff --cached --quiet; then
      git -C "$checkout_dir" pull --ff-only >/dev/null || true
    else
      echo "using existing dirty checkout: $checkout_dir" >&2
    fi
  elif [[ -e "$checkout_dir" ]]; then
    die "$checkout_dir exists but is not a git checkout"
  else
    git clone "$repo_url" "$checkout_dir" >/dev/null
  fi

  exec "$checkout_dir/install.sh" "$@"
fi

encrypted_key=""
expected_recipient=""

require_host() {
  [[ -n "$host" ]] || {
    usage
    exit 2
  }
}

load_host_key_context() {
  require_host

  encrypted_key="$repo_dir/secrets/host-keys/$host.txt"
  expected_recipient="$(
    awk -v host="$host" '
      $1 == "-" && $2 == "&" host { print $3; exit }
      $1 == "-" && $2 == "\\&" host { print $3; exit }
      $1 == "-" && $2 == ("&" host) { print $3; exit }
    ' "$repo_dir/.sops.yaml"
  )"

  [[ -f "$encrypted_key" ]] || die "missing encrypted host key: $encrypted_key"
  [[ -n "$expected_recipient" ]] || die "missing public recipient for host '$host' in $repo_dir/.sops.yaml"
}

decrypt_host_key() (
  load_host_key_context
  if ! { : > /dev/tty; } 2>/dev/null; then
    die "no interactive /dev/tty available for YubiKey PIN/touch prompt"
  fi
  command -v age-plugin-yubikey >/dev/null || die "age-plugin-yubikey is not in PATH"

  local age_bin identity_file identity_err
  age_bin="$(find_age)" || die "working age is not in PATH"

  identity_file="$(mktemp)"
  identity_err="$(mktemp)"
  trap 'rm -f "$identity_file" "$identity_err"' EXIT

  {
    echo
    echo "YubiKey required for host '$host'."
    echo "Plug in the YubiKey now. You may need to enter the PIN and touch it."
  } > /dev/tty

  if ! age-plugin-yubikey --identity 2> "$identity_err" \
    | awk '/^AGE-PLUGIN-YUBIKEY-/ { print; found = 1 } END { exit found ? 0 : 1 }' > "$identity_file"; then
    {
      echo
      echo "Could not read the YubiKey age identity."
      echo "Check that:"
      echo "  - the YubiKey is plugged in"
      echo "  - pcscd is running"
      echo "  - the key is not busy in another prompt"
      echo
      if [[ -s "$identity_err" ]]; then
        cat "$identity_err"
      fi
    } > /dev/tty
    exit 1
  fi

  if [[ ! -s "$identity_file" ]]; then
    echo "error: YubiKey identity output was empty" > /dev/tty
    exit 1
  fi

  "$age_bin" --decrypt --identity "$identity_file" "$encrypted_key" < /dev/tty
)

find_age() {
  local candidate
  for candidate in /usr/bin/age /bin/age "$(command -v age 2>/dev/null || true)"; do
    [[ -n "$candidate" ]] || continue
    "$candidate" --version >/dev/null 2>&1 || continue
    printf '%s\n' "$candidate"
    return 0
  done
  return 1
}

find_age_keygen() {
  local candidate
  for candidate in /usr/bin/age-keygen /bin/age-keygen "$(command -v age-keygen 2>/dev/null || true)"; do
    [[ -n "$candidate" ]] || continue
    "$candidate" -version >/dev/null 2>&1 || continue
    printf '%s\n' "$candidate"
    return 0
  done
  return 1
}

check_host_key() (
  load_host_key_context
  command -v age-plugin-yubikey >/dev/null || die "age-plugin-yubikey is not in PATH"

  local age_keygen tmp actual_recipient
  age_keygen="$(find_age_keygen)" || die "working age-keygen is not in PATH"

  tmp="$(mktemp -d)"
  trap 'rm -rf "$tmp"' EXIT

  decrypt_host_key > "$tmp/key.txt" || die "could not decrypt host key for $host"
  chmod 0600 "$tmp/key.txt"

  [[ -s "$tmp/key.txt" ]] || die "decrypted host key is empty for $host"

  actual_recipient="$("$age_keygen" -y "$tmp/key.txt")"
  [[ "$actual_recipient" == "$expected_recipient" ]] || {
    echo "expected: $expected_recipient" >&2
    echo "actual:   $actual_recipient" >&2
    die "decrypted host key does not match .sops.yaml recipient for $host"
  }

  echo "$actual_recipient"
)

write_generated_host_config() {
  local role="$1"
  local generated_host="$2"
  local generated_dir="$repo_dir/generated"
  local generated_host_file="$generated_dir/host.nix"

  install -d -m 0755 "$generated_dir"
  cat > "$generated_host_file" <<EOF
{ modulesPath, ... }:

{
  imports = [
    (modulesPath + "/installer/scan/not-detected.nix")
  ];

  networking.hostName = "$generated_host";

  bresilla.features.system.architecture = "unknown";
  bresilla.features.system.cpuVendor = "unknown";

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi = {
    canTouchEfiVariables = true;
    efiSysMountPoint = "/boot/efi";
  };
}
EOF
}

preflight_generated_install() {
  local role="$1"
  local generated_host="$2"
  local target="${3:-}"
  local flake_host="install-${role}-generated"

  host="$generated_host"

  echo "repo: $repo_dir"
  echo "host: $host"
  echo "machine type: $role"
  echo "flake: $flake_host"

  [[ -f "$repo_dir/generated/disko.nix" ]] || die "missing generated Disko file: $repo_dir/generated/disko.nix"
  [[ -f "$repo_dir/generated/host.nix" ]] || die "missing generated host file: $repo_dir/generated/host.nix"
  [[ -f "$repo_dir/secrets/hosts/$host.yaml" ]] || die "missing host secrets file: $repo_dir/secrets/hosts/$host.yaml"
  echo "repo files: ok"

  echo "YubiKey check: plug in the key, then follow PIN/touch prompts."
  actual_recipient="$(check_host_key)"
  echo "host key: ok ($actual_recipient)"

  command -v nix >/dev/null || die "nix is not in PATH"
  nix --extra-experimental-features 'nix-command flakes' eval \
    "$repo_dir#nixosConfigurations.$flake_host.config.sops.age.keyFile" >/dev/null
  nix --extra-experimental-features 'nix-command flakes' eval \
    "$repo_dir#nixosConfigurations.$flake_host.config.sops.secrets.\"netbird/setup_key\".path" >/dev/null
  echo "nix eval: ok"

  if [[ -n "$target" ]]; then
    command -v ssh >/dev/null || die "ssh is not in PATH"
    ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new "$target" true
    echo "ssh: ok ($target)"
  fi

  echo "preflight: ok"
}

remote_generated_install() {
  local role="$1"
  local generated_host="$2"
  local target="$3"
  local flake_host="install-${role}-generated"

  host="$generated_host"

  command -v nixos-anywhere >/dev/null || die "nixos-anywhere is not in PATH"
  command -v sops >/dev/null || die "sops is not in PATH"
  command -v age-plugin-yubikey >/dev/null || die "age-plugin-yubikey is not in PATH"

  tmp="$(mktemp -d)"
  trap 'rm -rf "$tmp"' EXIT

  install -d -m 0755 "$tmp/var/lib/sops-nix"
  decrypt_host_key > "$tmp/var/lib/sops-nix/key.txt"
  chmod 0600 "$tmp/var/lib/sops-nix/key.txt"

  nixos-anywhere \
    --extra-files "$tmp" \
    --flake "$repo_dir#$flake_host" \
    "$target"
}

find_gum() {
  if [[ -n "${GUM_BIN:-}" && -x "${GUM_BIN:-}" ]]; then
    printf '%s\n' "$GUM_BIN"
    return 0
  fi

  command -v gum 2>/dev/null && return 0

  if [[ -x /tmp/nixos-install-tools/gum ]]; then
    printf '%s\n' /tmp/nixos-install-tools/gum
    return 0
  fi

  return 1
}

download_gum() {
  local os arch asset_pattern api url tmp gum_bin
  os="$(uname -s)"
  arch="$(uname -m)"

  case "$os:$arch" in
    Linux:x86_64) asset_pattern='Linux_x86_64.tar.gz' ;;
    Linux:aarch64 | Linux:arm64) asset_pattern='Linux_arm64.tar.gz' ;;
    *) die "automatic gum download is not supported on $os/$arch; install gum manually" ;;
  esac

  command -v curl >/dev/null || die "curl is required to download gum"
  command -v tar >/dev/null || die "tar is required to unpack gum"

  api="$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest)"
  url="$(
    printf '%s\n' "$api" \
      | sed -nE 's/.*"browser_download_url": "([^"]*'"$asset_pattern"')".*/\1/p' \
      | head -n 1
  )"

  [[ -n "$url" ]] || die "could not find gum release asset matching $asset_pattern"

  tmp="$(mktemp -d)"
  trap 'rm -rf "$tmp"' RETURN
  curl -fsSL "$url" -o "$tmp/gum.tar.gz"
  tar -xzf "$tmp/gum.tar.gz" -C "$tmp"

  gum_bin="$(find "$tmp" -type f -name gum -perm -111 | head -n 1)"
  [[ -n "$gum_bin" ]] || die "gum archive did not contain an executable gum binary"

  install -d -m 0755 /tmp/nixos-install-tools
  install -m 0755 "$gum_bin" /tmp/nixos-install-tools/gum
  printf '%s\n' /tmp/nixos-install-tools/gum
}

ensure_gum() {
  find_gum || download_gum
}

choose_host() {
  local gum host_choice header
  header="${1:-system config}"
  gum="$(ensure_gum)"
  host_choice="$(
    find "$repo_dir/hosts" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
      | sort \
      | "$gum" choose --header "$header"
  )"
  [[ -n "$host_choice" ]] || die "no host selected"
  printf '%s\n' "$host_choice"
}

interactive_main() {
  local gum scope method target machine_type install_hostname mountpoint
  gum="$(ensure_gum)"

  scope="$(
    "$gum" choose --header "install target" \
      "LOCAL" \
      "REMOTE"
  )"
  [[ -n "$scope" ]] || die "no target selected"

  if [[ "$scope" == "REMOTE" ]]; then
    target="$("$gum" input --placeholder "nixos@192.168.100.163" --prompt "ssh target: ")"
    [[ -n "$target" ]] || die "ssh target is required"
  fi

  method="$(
    "$gum" choose --header "$scope layout" \
      "TEMPLATE" \
      "CONFIGURE"
  )"
  [[ -n "$method" ]] || die "no layout method selected"

  "$repo_dir/scripts/disko-wizard.sh" "$scope" "$method" "${target:-}"

  machine_type="$(
    "$gum" choose --header "machine type" \
      "laptop" \
      "server"
  )"
  [[ -n "$machine_type" ]] || die "machine type is required"

  install_hostname="$("$gum" input --placeholder "core" --prompt "hostname: ")"
  [[ -n "$install_hostname" ]] || die "hostname is required"

  write_generated_host_config "$machine_type" "$install_hostname"

  case "$scope" in
    REMOTE)
      preflight_generated_install "$machine_type" "$install_hostname" "$target"
      "$gum" confirm "Start remote install to $target as $install_hostname ($machine_type)?" || die "cancelled"
      remote_generated_install "$machine_type" "$install_hostname" "$target"
      ;;
    LOCAL)
      mountpoint="$("$gum" input --value "/mnt" --prompt "mountpoint: ")"
      [[ -n "$mountpoint" ]] || die "mountpoint is required"
      preflight_generated_install "$machine_type" "$install_hostname"
      "$gum" confirm "Drop local install secrets into $mountpoint for $install_hostname ($machine_type)?" || die "cancelled"
      host="$install_hostname"
      exec "$repo_dir/install.sh" local "$install_hostname" "$mountpoint"
      ;;
  esac
}

if [[ -z "$mode" ]]; then
  mode="interactive"
fi

case "$mode" in
  interactive)
    interactive_main
    ;;

  check)
    require_host
    load_host_key_context
    echo "repo: $repo_dir"
    echo "host: $host"
    echo "encrypted host key: $encrypted_key"
    echo "expected recipient: $expected_recipient"
    ;;

  key-check)
    require_host
    actual_recipient="$(check_host_key)"

    echo "repo: $repo_dir"
    echo "host: $host"
    echo "recipient: $actual_recipient"
    echo "key-check: ok"
    ;;

  preflight)
    generated_disko=false
    if [[ "$host" == "--generated-disko" ]]; then
      [[ "$#" -ge 4 ]] || {
        usage
        exit 2
      }
      generated_disko=true
      host="${3:-}"
      target="${4:-}"
    else
      target="${3:-}"
    fi
    require_host
    flake_host="$host"
    if [[ "$generated_disko" == true ]]; then
      flake_host="$host-generated"
      [[ -f "$repo_dir/generated/disko.nix" ]] || die "missing generated Disko file: $repo_dir/generated/disko.nix"
    fi

    echo "repo: $repo_dir"
    echo "host: $host"
    echo "flake: $flake_host"

    [[ -d "$repo_dir/hosts/$host" ]] || die "missing host directory: $repo_dir/hosts/$host"
    [[ -f "$repo_dir/hosts/$host/disko.nix" ]] || die "missing disko file: $repo_dir/hosts/$host/disko.nix"
    [[ -f "$repo_dir/secrets/hosts/$host.yaml" ]] || die "missing host secrets file: $repo_dir/secrets/hosts/$host.yaml"
    echo "repo files: ok"

    echo "YubiKey check: plug in the key, then follow PIN/touch prompts."
    actual_recipient="$(check_host_key)"
    echo "host key: ok ($actual_recipient)"

    command -v nix >/dev/null || die "nix is not in PATH"
    nix --extra-experimental-features 'nix-command flakes' eval \
      "$repo_dir#nixosConfigurations.$flake_host.config.sops.age.keyFile" >/dev/null
    nix --extra-experimental-features 'nix-command flakes' eval \
      "$repo_dir#nixosConfigurations.$flake_host.config.sops.secrets.\"netbird/setup_key\".path" >/dev/null
    echo "nix eval: ok"

    if [[ -n "$target" ]]; then
      command -v ssh >/dev/null || die "ssh is not in PATH"
      ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new "$target" true
      echo "ssh: ok ($target)"
    fi

    echo "preflight: ok"
    ;;

  remote)
    generated_disko=false
    if [[ "$host" == "--generated-disko" ]]; then
      [[ "$#" -ge 4 ]] || {
        usage
        exit 2
      }
      generated_disko=true
      host="${3:-}"
      target="${4:-}"
      shift 4
    else
      target="${3:-}"
      shift 3
    fi
    require_host
    [[ -n "$target" ]] || {
      usage
      exit 2
    }

    flake_host="$host"
    if [[ "$generated_disko" == true ]]; then
      flake_host="$host-generated"
      [[ -f "$repo_dir/generated/disko.nix" ]] || die "missing generated Disko file: $repo_dir/generated/disko.nix"
    fi

    command -v nixos-anywhere >/dev/null || die "nixos-anywhere is not in PATH"
    command -v sops >/dev/null || die "sops is not in PATH"
    command -v age-plugin-yubikey >/dev/null || die "age-plugin-yubikey is not in PATH"

    tmp="$(mktemp -d)"
    trap 'rm -rf "$tmp"' EXIT

    install -d -m 0755 "$tmp/var/lib/sops-nix"
    decrypt_host_key > "$tmp/var/lib/sops-nix/key.txt"
    chmod 0600 "$tmp/var/lib/sops-nix/key.txt"

    nixos-anywhere \
      --extra-files "$tmp" \
      --flake "$repo_dir#$flake_host" \
      "$@" \
      "$target"
    ;;

  local)
    require_host
    mountpoint="${3:-}"
    [[ -n "$mountpoint" ]] || {
      usage
      exit 2
    }

    command -v sops >/dev/null || die "sops is not in PATH"
    command -v age-plugin-yubikey >/dev/null || die "age-plugin-yubikey is not in PATH"
    [[ -d "$mountpoint" ]] || die "mountpoint does not exist: $mountpoint"

    install -d -m 0755 "$mountpoint/var/lib/sops-nix"
    decrypt_host_key > "$mountpoint/var/lib/sops-nix/key.txt"
    chmod 0600 "$mountpoint/var/lib/sops-nix/key.txt"
    ;;

  *)
    usage
    exit 2
    ;;
esac
