/**
 * One-shot SSH-exec helper into the Coolify host.
 *
 * Used to pull docker runtime data that the Coolify REST API doesn't
 * surface cleanly (per-service logs for compose apps, container state,
 * etc.). Strictly command-in / string-out — we never hold a persistent
 * channel and we never expose an interactive shell to callers.
 *
 * Required env (injected into vibn-frontend at deploy time):
 *   COOLIFY_SSH_HOST              e.g. 34.19.250.135 or coolify.vibnai.com
 *   COOLIFY_SSH_PORT              default 22
 *   COOLIFY_SSH_USER              default "vibn-logs"
 *   COOLIFY_SSH_PRIVATE_KEY_B64   base64-encoded PEM private key
 *
 * The authorised key lives in ~vibn-logs/.ssh/authorized_keys on the
 * Coolify host. That user must be a member of the `docker` group so
 * `docker logs`/`docker ps` work without sudo; it should NOT have
 * sudo rights.
 *
 * Aggregate probe for monitors: GET /api/internal/infra-health with INFRA_HEALTH_SECRET.
 */

import { Client, type ConnectConfig } from 'ssh2';

const DEFAULT_EXEC_TIMEOUT_MS = 10_000;
const DEFAULT_MAX_BYTES = 1_000_000; // 1 MB – more than enough for `docker logs --tail 2000`

export interface CoolifySshResult {
  code: number | null;
  stdout: string;
  stderr: string;
  truncated: boolean;
}

export interface CoolifySshOptions {
  /** Millis to wait for the command to finish before killing the channel. */
  timeoutMs?: number;
  /** Cap combined stdout+stderr. Extra bytes are dropped and `truncated=true`. */
  maxBytes?: number;
}

function loadConnectConfig(): ConnectConfig {
  const host = process.env.COOLIFY_SSH_HOST;
  const keyB64 = process.env.COOLIFY_SSH_PRIVATE_KEY_B64;
  if (!host) throw new Error('COOLIFY_SSH_HOST is not set');
  if (!keyB64) throw new Error('COOLIFY_SSH_PRIVATE_KEY_B64 is not set');
  const privateKey = Buffer.from(keyB64, 'base64').toString('utf8');
  const port = Number(process.env.COOLIFY_SSH_PORT ?? 22);
  const username = process.env.COOLIFY_SSH_USER ?? 'vibn-logs';
  return {
    host,
    port,
    username,
    privateKey,
    readyTimeout: 8_000,
    // We accept the host key on first connect; we're always connecting
    // to infra we own, but a future hardening pass should pin the host
    // key fingerprint via `hostVerifier`.
  };
}

/**
 * Run a single command on the Coolify host and collect its output.
 *
 * The helper enforces:
 *   - a wall-clock timeout (default 10s)
 *   - a combined-output byte cap (default 1 MB)
 *   - always-close semantics (connection is destroyed on return / throw)
 */
export function runOnCoolifyHost(
  command: string,
  opts: CoolifySshOptions = {},
): Promise<CoolifySshResult> {
  const cfg = loadConnectConfig();
  const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;

  return new Promise((resolve, reject) => {
    const conn = new Client();
    let settled = false;
    let stdout = '';
    let stderr = '';
    let truncated = false;
    const settle = (fn: () => void) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      try { conn.end(); } catch { /* noop */ }
      fn();
    };
    const timer = setTimeout(() => {
      settle(() => reject(new Error(`[coolify-ssh] timeout after ${timeoutMs}ms running: ${command}`)));
    }, timeoutMs);

    conn
      .on('ready', () => {
        conn.exec(command, (err, stream) => {
          if (err) return settle(() => reject(err));
          stream
            .on('close', (code: number | null) => {
              settle(() => resolve({ code, stdout, stderr, truncated }));
            })
            .on('data', (chunk: Buffer) => {
              if (stdout.length + stderr.length + chunk.length > maxBytes) {
                truncated = true;
                const room = Math.max(0, maxBytes - stdout.length - stderr.length);
                stdout += chunk.slice(0, room).toString('utf8');
              } else {
                stdout += chunk.toString('utf8');
              }
            })
            .stderr.on('data', (chunk: Buffer) => {
              if (stdout.length + stderr.length + chunk.length > maxBytes) {
                truncated = true;
                const room = Math.max(0, maxBytes - stdout.length - stderr.length);
                stderr += chunk.slice(0, room).toString('utf8');
              } else {
                stderr += chunk.toString('utf8');
              }
            });
        });
      })
      .on('error', (err) => settle(() => reject(err)))
      .connect(cfg);
  });
}

/**
 * True if the env is configured to talk to the Coolify host.
 * Callers use this to gracefully degrade when SSH isn't available
 * (e.g. local dev without the key pair mounted).
 */
export function isCoolifySshConfigured(): boolean {
  return !!(process.env.COOLIFY_SSH_HOST && process.env.COOLIFY_SSH_PRIVATE_KEY_B64);
}
