#!/usr/bin/env node
import { existsSync } from "node:fs";
import { stdin, stdout } from "node:process";
import readline from "node:readline/promises";
import {
  effectiveValue,
  effectiveValues,
  listDirectSshAliases,
  resolveOpenSshAlias,
} from "../src/import.ts";
import { expandUserPath } from "../src/config.ts";
import { probeHostKey, Ssh2Transport } from "../src/ssh2-transport.ts";
import { loadVaultOrEmpty, rotateVaultKey, saveVault } from "../src/vault.ts";

async function question(prompt) {
  const rl = readline.createInterface({ input: stdin, output: stdout });
  try {
    return (await rl.question(prompt)).trim();
  } finally {
    rl.close();
  }
}

async function hiddenQuestion(prompt) {
  if (!stdin.isTTY || !stdout.isTTY || typeof stdin.setRawMode !== "function") {
    throw new Error("secret input requires an interactive terminal");
  }
  stdout.write(prompt);
  stdin.setRawMode(true);
  stdin.resume();
  let value = "";
  try {
    return await new Promise((resolve, reject) => {
      const onData = (chunk) => {
        const text = chunk.toString("utf8");
        for (const char of text) {
          if (char === "\u0003") {
            stdin.off("data", onData);
            stdout.write("\n");
            reject(new Error("cancelled"));
            return;
          }
          if (char === "\r" || char === "\n") {
            stdin.off("data", onData);
            stdout.write("\n");
            resolve(value);
            return;
          }
          if (char === "\u007f" || char === "\b") value = value.slice(0, -1);
          else if (char >= " ") value += char;
        }
      };
      stdin.on("data", onData);
    });
  } finally {
    stdin.setRawMode(false);
    stdin.pause();
  }
}

async function confirm(prompt, defaultYes = false) {
  const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
  const value = (await question(`${prompt}${suffix}`)).toLowerCase();
  return value ? value === "y" || value === "yes" : defaultYes;
}

function requireValue(config, key, alias) {
  const value = effectiveValue(config, key);
  if (!value) throw new Error(`ssh -G ${alias} did not provide ${key}`);
  return value;
}

async function chooseAliases(arguments_) {
  if (arguments_.length > 0) return arguments_;
  const aliases = listDirectSshAliases();
  if (aliases.length === 0) {
    const explicit = await question("SSH Host alias to import: ");
    if (!explicit) throw new Error("no SSH alias selected");
    return [explicit];
  }
  console.log("Available SSH aliases:");
  aliases.forEach((alias, index) => console.log(`  ${index + 1}. ${alias}`));
  const selection = await question("Select numbers or aliases (comma-separated): ");
  const chosen = selection.split(",").map((value) => value.trim()).filter(Boolean).map((value) => {
    const index = Number.parseInt(value, 10);
    return Number.isInteger(index) && String(index) === value && aliases[index - 1] ? aliases[index - 1] : value;
  });
  if (chosen.length === 0) throw new Error("no SSH alias selected");
  return [...new Set(chosen)];
}

async function importHost(config, alias) {
  const effective = resolveOpenSshAlias(alias);
  const proxyJump = effectiveValue(effective, "proxyjump");
  const proxyCommand = effectiveValue(effective, "proxycommand");
  if (proxyJump && proxyJump !== "none") throw new Error(`${alias} uses ProxyJump, which is not supported yet`);
  if (proxyCommand && proxyCommand !== "none") throw new Error(`${alias} uses ProxyCommand, which is not supported yet`);

  const hostName = requireValue(effective, "hostname", alias);
  const user = requireValue(effective, "user", alias);
  const port = Number.parseInt(requireValue(effective, "port", alias), 10);
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${alias} has an invalid port`);
  console.log(`\n${alias}: ${user}@${hostName}:${port}`);

  const identityFiles = effectiveValues(effective, "identityfile")
    .map((path) => path.replace(/^"|"$/g, ""))
    .filter((path) => existsSync(expandUserPath(path)));
  const defaultMode = identityFiles.length > 0 ? "key" : "password";
  const modeInput = (await question(`Authentication [key/password] (${defaultMode}): `)).toLowerCase();
  const mode = modeInput || defaultMode;
  let auth;
  if (mode === "key") {
    const suggested = identityFiles[0] ?? "";
    const identityFile = await question(`Private key path${suggested ? ` (${suggested})` : ""}: `) || suggested;
    if (!identityFile || !existsSync(expandUserPath(identityFile))) throw new Error(`private key does not exist: ${identityFile}`);
    const encrypted = await confirm("Does this private key require a passphrase?");
    const passphrase = encrypted ? await hiddenQuestion("Private key passphrase: ") : undefined;
    if (encrypted && !passphrase) throw new Error("private key passphrase cannot be empty");
    auth = { type: "private-key", identityFile, ...(passphrase ? { passphrase } : {}) };
  } else if (mode === "password") {
    const password = await hiddenQuestion("Server password: ");
    if (!password) throw new Error("server password cannot be empty");
    auth = { type: "password", password, method: "auto" };
  } else {
    throw new Error("authentication must be key or password");
  }

  console.log("Obtaining SSH host key fingerprint...");
  const hostKey = await probeHostKey(hostName, port, user);
  console.log(`Host key: ${hostKey.algorithm} ${hostKey.fingerprint}`);
  if (!await confirm("Trust and pin this host key?")) throw new Error(`host key for ${alias} was not trusted`);

  const label = await question(`Display label (${config.hosts[alias]?.label ?? alias}): `) || config.hosts[alias]?.label || alias;
  const host = { label, sourceAlias: alias, hostName, user, port, auth, hostKey };
  const transport = new Ssh2Transport(host);
  try {
    console.log("Testing SSH2 authentication...");
    await transport.connect();
    const result = await transport.capture('printf "%s\\n%s" "$HOME" "$(pwd)"', ".", 20);
    if (result.exitCode !== 0) throw new Error("remote HOME/cwd probe failed");
    const [remoteHome, remoteCwd] = result.output.toString("utf8").trim().split(/\r?\n/, 2);
    if (!remoteHome || !remoteCwd) throw new Error("remote HOME/cwd probe returned incomplete output");
    host.defaultCwd = remoteCwd;
    console.log(`Connected successfully; default cwd: ${remoteCwd}`);
  } finally {
    await transport.dispose();
  }

  config.hosts[alias] = host;
  const groupId = await question("Group id (optional): ");
  if (groupId) {
    const groupLabel = config.groups?.[groupId]?.label || await question(`Group label (${groupId}): `) || groupId;
    config.groups ??= {};
    const hosts = new Set(config.groups[groupId]?.hosts ?? []);
    hosts.add(alias);
    config.groups[groupId] = { label: groupLabel, hosts: [...hosts] };
  }
}

async function main() {
  const [command = "list", ...arguments_] = process.argv.slice(2);
  if (command === "list") {
    const config = loadVaultOrEmpty();
    const entries = Object.entries(config.hosts);
    if (entries.length === 0) {
      console.log("No pi-ssh hosts configured.");
      return;
    }
    for (const [id, host] of entries) console.log(`${id}\t${host.label ?? id}\t${host.user}@${host.hostName}:${host.port}\t${host.auth.type}`);
    return;
  }
  if (command === "rotate-key") {
    rotateVaultKey();
    console.log("Rotated pi-ssh vault key.");
    return;
  }
  if (command === "remove") {
    const id = arguments_[0];
    if (!id) throw new Error("usage: ssh_config.sh remove <host-id>");
    const config = loadVaultOrEmpty();
    if (!config.hosts[id]) throw new Error(`unknown pi-ssh host: ${id}`);
    delete config.hosts[id];
    for (const group of Object.values(config.groups ?? {})) group.hosts = group.hosts.filter((host) => host !== id);
    saveVault(config);
    console.log(`Removed ${id}.`);
    return;
  }
  if (command !== "import" && command !== "update") {
    throw new Error("usage: ssh_config.sh [list|import [alias...]|update <alias>|remove <id>|rotate-key]");
  }
  const config = loadVaultOrEmpty();
  const aliases = command === "update"
    ? [arguments_[0] || (() => { throw new Error("usage: ssh_config.sh update <alias>"); })()]
    : await chooseAliases(arguments_);
  for (const alias of aliases) await importHost(config, alias);
  saveVault(config);
  console.log(`Saved ${aliases.length} host(s) to the encrypted pi-ssh vault.`);
}

main().catch((error) => {
  console.error(`pi-ssh config: ${error instanceof Error ? error.message : String(error)}`);
  process.exitCode = 1;
});
