/**
 * SSH preflight: resolve + check + connect to a pinned IP.
 *
 * Design rationale (rev 4):
 *   The Node ssh2 client normally takes (host, port) and does its own DNS
 *   resolution. We instead resolve once with policy enforcement, then hand
 *   ssh2 a pre-connected socket pinned to the resolved literal IP. This
 *   prevents DNS rebinding between policy-check time and connect time.
 *
 *   `allowPrivate` is the OR of the global config flag and the per-connection
 *   flag (admin-only). Callers compute the OR; this module is just the gate.
 *
 * Plan: docs/superpowers/plans/2026-05-12-ssh-tool-integration.md (Phase 2).
 */
import type * as net from 'node:net';
import {
  resolveAndCheck,
  pinnedConnect,
  type LookupFn,
} from '../net/ssrf-strict.js';

export interface PreflightAndConnectArgs {
  host: string;
  port: number;
  allowPrivate: boolean;
  timeoutMs: number;
  /** Test seam. */
  lookup?: LookupFn;
}

export interface PreflightResult {
  socket: net.Socket;
  resolvedIp: string;
  family: 4 | 6;
}

export class SshSsrfError extends Error {
  readonly code:
    | 'invalid_host'
    | 'forbidden_address'
    | 'dns_failed'
    | 'connect_failed'
    | 'connect_timeout';
  constructor(code: SshSsrfError['code'], message: string) {
    super(message);
    this.code = code;
    this.name = 'SshSsrfError';
  }
}

export async function preflightAndConnect(args: PreflightAndConnectArgs): Promise<PreflightResult> {
  if (!Number.isInteger(args.port) || args.port < 1 || args.port > 65535) {
    throw new SshSsrfError('invalid_host', `Invalid port: ${args.port}`);
  }
  if (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0) {
    throw new SshSsrfError('invalid_host', 'timeoutMs must be > 0');
  }

  const resolved = await resolveAndCheck({
    host: args.host,
    allowPrivate: args.allowPrivate,
    lookup: args.lookup,
  });
  if (!resolved.ok) {
    // Distinguish DNS failure vs policy rejection for clearer audit reasons.
    if (/^DNS/.test(resolved.reason)) {
      throw new SshSsrfError('dns_failed', resolved.reason);
    }
    if (/Invalid host/i.test(resolved.reason)) {
      throw new SshSsrfError('invalid_host', resolved.reason);
    }
    throw new SshSsrfError('forbidden_address', resolved.reason);
  }

  let socket: net.Socket;
  try {
    socket = await pinnedConnect({ ip: resolved.ip, port: args.port, timeoutMs: args.timeoutMs });
  } catch (err) {
    const e = err as Error;
    if (e.message === 'connect_timeout') {
      throw new SshSsrfError('connect_timeout', `Connect timed out after ${args.timeoutMs}ms`);
    }
    throw new SshSsrfError('connect_failed', e.message);
  }
  return { socket, resolvedIp: resolved.ip, family: resolved.family };
}
