import { homedir } from "node:os";
import { isAbsolute, join, resolve } from "node:path";

export interface PasswordAuthConfig {
  type: "password";
  password: string;
  method?: "auto" | "password" | "keyboard-interactive";
}

export interface PrivateKeyAuthConfig {
  type: "private-key";
  identityFile: string;
  passphrase?: string;
}

export type SshAuthConfig = PasswordAuthConfig | PrivateKeyAuthConfig;

export interface SshHostConfig {
  label?: string;
  sourceAlias?: string;
  hostName: string;
  user: string;
  port: number;
  defaultCwd?: string;
  auth: SshAuthConfig;
  hostKey: {
    algorithm: string;
    fingerprint: string;
  };
}

export interface SshGroupConfig {
  label: string;
  hosts: string[];
}

export interface PiSshConfig {
  version: 1;
  hosts: Record<string, SshHostConfig>;
  groups?: Record<string, SshGroupConfig>;
}

export interface VaultPaths {
  directory: string;
  encryptedConfig: string;
  key: string;
}

const HOST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;

export function resolveVaultPaths(
  env: NodeJS.ProcessEnv = process.env,
  platform: NodeJS.Platform = process.platform,
  home = homedir(),
): VaultPaths {
  const base = platform === "win32"
    ? env.APPDATA || join(home, "AppData", "Roaming")
    : env.XDG_CONFIG_HOME || join(home, ".config");
  const directory = join(base, "my-pi", "pi-ssh");
  return {
    directory,
    encryptedConfig: join(directory, "hosts.enc"),
    key: join(directory, "vault.key"),
  };
}

export function expandUserPath(value: string, home = homedir()): string {
  if (value === "~") return home;
  if (value.startsWith("~/")) return join(home, value.slice(2));
  return isAbsolute(value) ? value : resolve(home, value);
}

function record(value: unknown, name: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new Error(`${name} must be an object`);
  }
  return value as Record<string, unknown>;
}

function nonEmptyString(value: unknown, name: string): string {
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new Error(`${name} must be a non-empty string`);
  }
  return value;
}

function optionalString(value: unknown, name: string): string | undefined {
  return value === undefined ? undefined : nonEmptyString(value, name);
}

function validateAuth(value: unknown, name: string): SshAuthConfig {
  const auth = record(value, name);
  if (auth.type === "password") {
    const method = auth.method;
    if (method !== undefined && method !== "auto" && method !== "password" && method !== "keyboard-interactive") {
      throw new Error(`${name}.method must be auto, password, or keyboard-interactive`);
    }
    return {
      type: "password",
      password: nonEmptyString(auth.password, `${name}.password`),
      ...(method === undefined ? {} : { method }),
    };
  }
  if (auth.type === "private-key") {
    return {
      type: "private-key",
      identityFile: nonEmptyString(auth.identityFile, `${name}.identityFile`),
      ...(auth.passphrase === undefined ? {} : { passphrase: nonEmptyString(auth.passphrase, `${name}.passphrase`) }),
    };
  }
  throw new Error(`${name}.type must be password or private-key`);
}

function validateHost(value: unknown, name: string): SshHostConfig {
  const host = record(value, name);
  const port = host.port;
  if (!Number.isInteger(port) || (port as number) < 1 || (port as number) > 65535) {
    throw new Error(`${name}.port must be an integer from 1 to 65535`);
  }
  const hostKey = record(host.hostKey, `${name}.hostKey`);
  const fingerprint = nonEmptyString(hostKey.fingerprint, `${name}.hostKey.fingerprint`);
  if (!fingerprint.startsWith("SHA256:")) {
    throw new Error(`${name}.hostKey.fingerprint must use SHA256 format`);
  }
  return {
    ...(host.label === undefined ? {} : { label: optionalString(host.label, `${name}.label`) }),
    ...(host.sourceAlias === undefined ? {} : { sourceAlias: optionalString(host.sourceAlias, `${name}.sourceAlias`) }),
    hostName: nonEmptyString(host.hostName, `${name}.hostName`),
    user: nonEmptyString(host.user, `${name}.user`),
    port: port as number,
    ...(host.defaultCwd === undefined ? {} : { defaultCwd: optionalString(host.defaultCwd, `${name}.defaultCwd`) }),
    auth: validateAuth(host.auth, `${name}.auth`),
    hostKey: {
      algorithm: nonEmptyString(hostKey.algorithm, `${name}.hostKey.algorithm`),
      fingerprint,
    },
  };
}

export function validatePiSshConfig(value: unknown): PiSshConfig {
  const root = record(value, "config");
  if (root.version !== 1) throw new Error("config.version must be 1");
  const hostsValue = record(root.hosts, "config.hosts");
  const hosts: Record<string, SshHostConfig> = {};
  for (const [id, host] of Object.entries(hostsValue)) {
    if (!HOST_ID_PATTERN.test(id)) throw new Error(`invalid host id: ${id}`);
    hosts[id] = validateHost(host, `config.hosts.${id}`);
  }

  let groups: Record<string, SshGroupConfig> | undefined;
  if (root.groups !== undefined) {
    groups = {};
    for (const [id, value] of Object.entries(record(root.groups, "config.groups"))) {
      if (!HOST_ID_PATTERN.test(id)) throw new Error(`invalid group id: ${id}`);
      const group = record(value, `config.groups.${id}`);
      if (!Array.isArray(group.hosts) || group.hosts.some((host) => typeof host !== "string" || !hosts[host])) {
        throw new Error(`config.groups.${id}.hosts must reference configured hosts`);
      }
      groups[id] = {
        label: nonEmptyString(group.label, `config.groups.${id}.label`),
        hosts: [...new Set(group.hosts as string[])],
      };
    }
  }

  return { version: 1, hosts, ...(groups === undefined ? {} : { groups }) };
}

export function emptyPiSshConfig(): PiSshConfig {
  return { version: 1, hosts: {} };
}
