/**
 * Run a one-shot command inside a Coolify-managed app container.
 *
 * Same SSH backbone as lib/coolify-logs.ts: we connect to the Coolify
 * host as the locked-down `vibn-logs` user, resolve the target
 * container via listContainersForApp, and execute `docker exec` in
 * non-interactive mode. No TTY, no stdin — this is purpose-built for
 * "run a command, return the output" operator actions (running
 * migrations, sanity checks, one-off CLI invocations).
 *
 * Tenant safety: every caller must verify the app uuid belongs to the
 * calling workspace BEFORE invoking this helper (via
 * getApplicationInProject). This file trusts that check has already
 * happened.
 */

import { runOnCoolifyHost } from "@/lib/coolify-ssh";
import {
  resolveAppTargetContainer,
  type ComposeContainer,
} from "@/lib/coolify-containers";

const DEFAULT_TIMEOUT_MS = 60_000;
const MAX_TIMEOUT_MS = 600_000; // 10 min — enough for migrations / seeds
const DEFAULT_MAX_BYTES = 1_000_000; // 1 MB combined stdout+stderr
const MAX_BYTES_CAP = 5_000_000; // 5 MB hard ceiling

export interface ExecInAppOptions {
  appUuid: string;
  /** Compose service name (`server`, `db`, `worker`, …). Required when the app has >1 container. */
  service?: string;
  /** The command to run. Passed through `sh -lc`, so shell syntax (pipes, redirects, `&&`) works. */
  command: string;
  /** Kill the channel after this many ms. Defaults to 60s, max 10 min. */
  timeoutMs?: number;
  /** Cap combined stdout+stderr. Defaults to 1 MB, max 5 MB. */
  maxBytes?: number;
  /** Optional `--user` passed to `docker exec`, e.g. `root` or `1000:1000`. */
  user?: string;
  /** Optional `--workdir` passed to `docker exec`, e.g. `/app`. */
  workdir?: string;
}

export interface ExecInAppResult {
  container: string;
  service: string;
  code: number | null;
  stdout: string;
  stderr: string;
  truncated: boolean;
  durationMs: number;
  /** Container health at time of exec (parsed from `docker ps`). */
  containerHealth: ComposeContainer["health"];
  /** The command as it was actually executed (post-escape, for logs). */
  executedCommand: string;
}

/** Shell-escape a single token for bash. */
function sq(s: string): string {
  return `'${s.replace(/'/g, `'\\''`)}'`;
}

export async function execInCoolifyApp(
  opts: ExecInAppOptions,
): Promise<ExecInAppResult> {
  if (!opts.command || typeof opts.command !== "string") {
    throw new Error("command is required");
  }
  const timeoutMs = Math.min(
    Math.max(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, 1_000),
    MAX_TIMEOUT_MS,
  );
  const maxBytes = Math.min(
    Math.max(opts.maxBytes ?? DEFAULT_MAX_BYTES, 1_024),
    MAX_BYTES_CAP,
  );

  const container = await resolveAppTargetContainer(opts.appUuid, opts.service);

  // Build the `docker exec` invocation. We run the payload through
  // `sh -lc` so callers can use shell syntax naturally. The payload
  // itself is passed as a single-quoted argv token — no interpolation
  // happens outside of what the child shell does.
  const flags: string[] = [];
  if (opts.user) flags.push(`--user ${sq(opts.user)}`);
  if (opts.workdir) flags.push(`--workdir ${sq(opts.workdir)}`);

  // NOTE: do NOT collapse whitespace on the outer docker-exec invocation.
  // The command payload is already single-quoted via sq(), so only the
  // docker/flag portion needs normalising — and newlines in the payload
  // itself must be preserved so multi-line bash scripts work correctly.
  // (Collapsing \n → ' ' turns `if ... ; then\n  cmd\nfi` into invalid sh.)
  // Keep the trailing space before the quoted command — dash (Ubuntu's sh)
  // requires a space between -lc and its argument. Trimming it causes
  // sh to mis-parse the quoted string as flags ("Illegal option -d").
  const dockerPrefix =
    `docker exec ${flags.join(" ")} ${sq(container.name)} sh -lc `.replace(
      /  +/g,
      " ",
    );
  const executedCommand = `${dockerPrefix}${sq(opts.command)}`;

  const startedAt = Date.now();
  // Audit log: record the command + target, NOT the output (output
  // may contain secrets). Structured so downstream log shipping can
  // parse it.
  console.log(
    "[apps.exec]",
    JSON.stringify({
      app_uuid: opts.appUuid,
      container: container.name,
      service: container.service,
      timeout_ms: timeoutMs,
      command: opts.command,
    }),
  );

  const res = await runOnCoolifyHost(executedCommand, { timeoutMs, maxBytes });
  const durationMs = Date.now() - startedAt;

  // Our ssh helper merges stream metadata; we didn't redirect stderr
  // to stdout in the command, so the ssh-layer separation is already
  // correct. But docker-exec uses exit code 126/127 for "cannot exec"
  // and >0 for the user's command failing; surface all of them.
  return {
    container: container.name,
    service: container.service,
    code: res.code,
    stdout: res.stdout,
    stderr: res.stderr,
    truncated: res.truncated,
    durationMs,
    containerHealth: container.health,
    executedCommand,
  };
}
