import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import {
  chmodSync,
  existsSync,
  mkdirSync,
  readFileSync,
  renameSync,
  statSync,
  unlinkSync,
  writeFileSync,
} from "node:fs";
import { basename, join } from "node:path";
import {
  emptyPiSshConfig,
  resolveVaultPaths,
  validatePiSshConfig,
  type PiSshConfig,
  type VaultPaths,
} from "./config.ts";

const FORMAT = "my-pi-ssh-v1";
const KEY_BYTES = 32;
const IV_BYTES = 12;

interface EncryptedEnvelope {
  format: typeof FORMAT;
  iv: string;
  tag: string;
  ciphertext: string;
}

function assertOwnerOnly(path: string): void {
  if (process.platform === "win32") return;
  const mode = statSync(path).mode & 0o777;
  if ((mode & 0o077) !== 0) {
    throw new Error(`${basename(path)} permissions must be 600 or stricter`);
  }
}

function ensureDirectory(paths: VaultPaths): void {
  mkdirSync(paths.directory, { recursive: true, mode: 0o700 });
  if (process.platform !== "win32") chmodSync(paths.directory, 0o700);
}

function parseKey(text: string): Buffer {
  const normalized = text.trim();
  const key = Buffer.from(normalized, "base64");
  if (key.length !== KEY_BYTES || key.toString("base64") !== normalized) {
    throw new Error("vault.key is invalid");
  }
  return key;
}

function readKey(paths: VaultPaths): Buffer {
  if (!existsSync(paths.key)) throw new Error("pi-ssh vault key is missing; run ssh_config.sh import");
  assertOwnerOnly(paths.key);
  return parseKey(readFileSync(paths.key, "utf8"));
}

function getOrCreateKey(paths: VaultPaths): Buffer {
  ensureDirectory(paths);
  if (existsSync(paths.key)) return readKey(paths);
  const key = randomBytes(KEY_BYTES);
  writeFileSync(paths.key, `${key.toString("base64")}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
  if (process.platform !== "win32") chmodSync(paths.key, 0o600);
  return key;
}

function parseEnvelope(text: string): EncryptedEnvelope {
  let value: unknown;
  try {
    value = JSON.parse(text);
  } catch {
    throw new Error("pi-ssh encrypted configuration is invalid JSON");
  }
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new Error("pi-ssh encrypted configuration is invalid");
  }
  const envelope = value as Record<string, unknown>;
  for (const field of ["iv", "tag", "ciphertext"] as const) {
    if (typeof envelope[field] !== "string" || envelope[field].length === 0) {
      throw new Error(`pi-ssh encrypted configuration is missing ${field}`);
    }
  }
  if (envelope.format !== FORMAT) throw new Error("unsupported pi-ssh encrypted configuration format");
  return envelope as unknown as EncryptedEnvelope;
}

export function encryptConfig(config: PiSshConfig, key: Buffer): string {
  if (key.length !== KEY_BYTES) throw new Error("pi-ssh vault key must be 32 bytes");
  const validated = validatePiSshConfig(config);
  const iv = randomBytes(IV_BYTES);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  cipher.setAAD(Buffer.from(FORMAT, "utf8"));
  const plaintext = Buffer.from(JSON.stringify(validated), "utf8");
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const envelope: EncryptedEnvelope = {
    format: FORMAT,
    iv: iv.toString("base64"),
    tag: cipher.getAuthTag().toString("base64"),
    ciphertext: ciphertext.toString("base64"),
  };
  return `${JSON.stringify(envelope, null, 2)}\n`;
}

export function decryptConfig(encrypted: string, key: Buffer): PiSshConfig {
  if (key.length !== KEY_BYTES) throw new Error("pi-ssh vault key must be 32 bytes");
  const envelope = parseEnvelope(encrypted);
  try {
    const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
    decipher.setAAD(Buffer.from(FORMAT, "utf8"));
    decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
    const plaintext = Buffer.concat([
      decipher.update(Buffer.from(envelope.ciphertext, "base64")),
      decipher.final(),
    ]).toString("utf8");
    return validatePiSshConfig(JSON.parse(plaintext));
  } catch (error) {
    if (error instanceof SyntaxError) throw new Error("decrypted pi-ssh configuration is invalid JSON");
    throw new Error("pi-ssh configuration could not be decrypted or failed integrity verification");
  }
}

export function loadVault(paths: VaultPaths = resolveVaultPaths()): PiSshConfig {
  if (!existsSync(paths.encryptedConfig)) {
    throw new Error("pi-ssh is not configured; run ssh_config.sh import");
  }
  assertOwnerOnly(paths.encryptedConfig);
  const key = readKey(paths);
  return decryptConfig(readFileSync(paths.encryptedConfig, "utf8"), key);
}

export function loadVaultOrEmpty(paths: VaultPaths = resolveVaultPaths()): PiSshConfig {
  return existsSync(paths.encryptedConfig) ? loadVault(paths) : emptyPiSshConfig();
}

export function saveVault(config: PiSshConfig, paths: VaultPaths = resolveVaultPaths()): void {
  const key = getOrCreateKey(paths);
  const encrypted = encryptConfig(config, key);
  const temp = join(paths.directory, `.hosts.enc.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
  try {
    writeFileSync(temp, encrypted, { encoding: "utf8", mode: 0o600, flag: "wx" });
    if (process.platform !== "win32") chmodSync(temp, 0o600);
    renameSync(temp, paths.encryptedConfig);
    if (process.platform !== "win32") chmodSync(paths.encryptedConfig, 0o600);
  } finally {
    if (existsSync(temp)) unlinkSync(temp);
  }
}

export function rotateVaultKey(paths: VaultPaths = resolveVaultPaths()): void {
  const config = loadVault(paths);
  const newKey = randomBytes(KEY_BYTES);
  const suffix = `${process.pid}.${randomBytes(6).toString("hex")}`;
  const keyTemp = join(paths.directory, `.vault.key.${suffix}.tmp`);
  const configTemp = join(paths.directory, `.hosts.enc.${suffix}.tmp`);
  const keyBackup = join(paths.directory, `.vault.key.${suffix}.backup`);
  const configBackup = join(paths.directory, `.hosts.enc.${suffix}.backup`);
  let committed = false;
  try {
    writeFileSync(keyTemp, `${newKey.toString("base64")}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
    writeFileSync(configTemp, encryptConfig(config, newKey), { encoding: "utf8", mode: 0o600, flag: "wx" });
    renameSync(paths.key, keyBackup);
    renameSync(paths.encryptedConfig, configBackup);
    renameSync(keyTemp, paths.key);
    renameSync(configTemp, paths.encryptedConfig);
    committed = true;
  } finally {
    if (!committed) {
      if (existsSync(keyBackup)) {
        if (existsSync(paths.key)) unlinkSync(paths.key);
        renameSync(keyBackup, paths.key);
      }
      if (existsSync(configBackup)) {
        if (existsSync(paths.encryptedConfig)) unlinkSync(paths.encryptedConfig);
        renameSync(configBackup, paths.encryptedConfig);
      }
    }
    for (const path of [keyTemp, configTemp, keyBackup, configBackup]) {
      if (existsSync(path)) unlinkSync(path);
    }
  }
}
