import { spawn } from 'node:child_process';
import net from 'node:net';

export type SshParsedTarget = {
  user?: string;
  host: string;
  port: number;
};

export type SshTunnel = {
  parsedTarget: SshParsedTarget;
  localPort: number;
  remotePort: number;
  pid: number | null;
  stderr: string[];
  stop: () => Promise<void>;
};

export function parseSshTarget(raw: string): SshParsedTarget | null {
  const trimmed = raw.trim().replace(/^ssh\s+/, '');
  if (!trimmed) {
    return null;
  }

  const [userPart, hostPart] = trimmed.includes('@')
    ? ((): [string | undefined, string] => {
        const idx = trimmed.indexOf('@');
        const user = trimmed.slice(0, idx).trim();
        const host = trimmed.slice(idx + 1).trim();
        return [user || undefined, host];
      })()
    : [undefined, trimmed];

  const colonIdx = hostPart.lastIndexOf(':');
  if (colonIdx > 0 && colonIdx < hostPart.length - 1) {
    const host = hostPart.slice(0, colonIdx).trim();
    const portRaw = hostPart.slice(colonIdx + 1).trim();
    const port = Number.parseInt(portRaw, 10);
    if (!host || !Number.isFinite(port) || port <= 0) {
      return null;
    }
    if (host.startsWith('-')) {
      return null;
    }
    return { user: userPart, host, port };
  }

  if (!hostPart || hostPart.startsWith('-')) {
    return null;
  }
  return { user: userPart, host: hostPart, port: 22 };
}

async function pickEphemeralPort(): Promise<number> {
  return new Promise<number>((resolve, reject) => {
    const server = net.createServer();
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      const addr = server.address();
      server.close(() => {
        if (!addr || typeof addr === 'string') {
          reject(new Error('failed to allocate a local port'));
          return;
        }
        resolve(addr.port);
      });
    });
  });
}

async function canBindLocalPort(port: number): Promise<boolean> {
  return new Promise<boolean>((resolve) => {
    const server = net.createServer();
    server.once('error', () => resolve(false));
    server.once('listening', () => {
      server.close(() => resolve(true));
    });
    server.listen(port, '127.0.0.1');
  });
}

async function canConnectLocal(port: number): Promise<boolean> {
  return new Promise<boolean>((resolve) => {
    const socket = net.connect({ host: '127.0.0.1', port });
    const done = (ok: boolean) => {
      socket.removeAllListeners();
      socket.destroy();
      resolve(ok);
    };
    socket.once('connect', () => done(true));
    socket.once('error', () => done(false));
    socket.setTimeout(250, () => done(false));
  });
}

async function waitForLocalListener(port: number, timeoutMs: number): Promise<void> {
  const startedAt = Date.now();
  while (Date.now() - startedAt < timeoutMs) {
    if (await canConnectLocal(port)) {
      return;
    }
    await new Promise((r) => setTimeout(r, 50));
  }
  throw new Error(`ssh tunnel did not start listening on localhost:${port}`);
}

export async function startSshPortForward(opts: {
  target: string;
  identity?: string;
  localPortPreferred: number;
  remotePort: number;
  timeoutMs?: number;
}): Promise<SshTunnel> {
  const parsed = parseSshTarget(opts.target);
  if (!parsed) {
    throw new Error(`invalid SSH target: ${opts.target}`);
  }

  let localPort = opts.localPortPreferred;
  if (!(await canBindLocalPort(localPort))) {
    localPort = await pickEphemeralPort();
  }

  const userHost = parsed.user ? `${parsed.user}@${parsed.host}` : parsed.host;
  const args = [
    '-N',
    '-L',
    `${localPort}:127.0.0.1:${opts.remotePort}`,
    '-p',
    String(parsed.port),
    '-o',
    'ExitOnForwardFailure=yes',
    '-o',
    'BatchMode=yes',
    '-o',
    'StrictHostKeyChecking=accept-new',
    '-o',
    'ConnectTimeout=5',
    '-o',
    'ServerAliveInterval=15',
    '-o',
    'ServerAliveCountMax=3',
  ];
  if (opts.identity?.trim()) {
    args.push('-i', opts.identity.trim());
  }
  args.push('--', userHost);

  const stderr: string[] = [];
  const sshBin = process.platform === 'win32' ? 'ssh' : '/usr/bin/ssh';
  const child = spawn(sshBin, args, {
    stdio: ['ignore', 'ignore', 'pipe'],
  });
  child.stderr?.setEncoding('utf8');
  child.stderr?.on('data', (chunk) => {
    const lines = String(chunk)
      .split('\n')
      .map((l) => l.trim())
      .filter(Boolean);
    stderr.push(...lines);
  });

  const stop = async () => {
    if (child.killed) {
      return;
    }
    child.kill('SIGTERM');
    await new Promise<void>((resolve) => {
      const t = setTimeout(() => {
        try {
          child.kill('SIGKILL');
        } finally {
          resolve();
        }
      }, 1500);
      child.once('exit', () => {
        clearTimeout(t);
        resolve();
      });
    });
  };

  const timeoutMs = opts.timeoutMs ?? 10_000;
  try {
    await Promise.race([
      waitForLocalListener(localPort, Math.max(250, timeoutMs)),
      new Promise<void>((_, reject) => {
        child.once('exit', (code, signal) => {
          reject(new Error(`ssh exited (${code ?? 'null'}${signal ? `/${signal}` : ''})`));
        });
      }),
    ]);
  } catch (err) {
    await stop();
    const suffix = stderr.length > 0 ? `\n${stderr.join('\n')}` : '';
    const em = err instanceof Error ? err.message : String(err);
    throw new Error(`${em}${suffix}`, { cause: err });
  }

  return {
    parsedTarget: parsed,
    localPort,
    remotePort: opts.remotePort,
    pid: typeof child.pid === 'number' ? child.pid : null,
    stderr,
    stop,
  };
}
