/**
 * Container enumeration for a Coolify-managed app.
 *
 * Coolify names compose containers as `{service}-{appUuid}-{shortHash}`
 * and single-container apps as `{name}-{appUuid}` (or similar). To
 * target a specific service we:
 *   1. SSH to the Coolify host as the locked-down vibn-logs user
 *   2. `docker ps --filter name={appUuid}` to list matching containers
 *   3. Derive the service name by stripping the uuid suffix
 *
 * Shared between lib/coolify-logs.ts (read path) and
 * lib/coolify-exec.ts (write path). Both pass through the same
 * ssh2-based helper in lib/coolify-ssh.ts.
 */

import { runOnCoolifyHost } from '@/lib/coolify-ssh';

export interface ComposeContainer {
  /** Full docker container name, e.g. `server-flz0i740ql0a-20113077`. */
  name: string;
  /** Derived compose service, e.g. `server`. */
  service: string;
  /** `Up 5 minutes (healthy)`, `Exited (1) 2 minutes ago`, etc. */
  status: string;
  /** `healthy` / `unhealthy` / `starting` / `none`. Parsed from status. */
  health: 'healthy' | 'unhealthy' | 'starting' | 'none';
}

/**
 * Shell-escape a single token for bash. We only ever pass Coolify-
 * derived uuids through here, but defence-in-depth is cheap.
 */
function sq(s: string): string {
  return `'${s.replace(/'/g, `'\\''`)}'`;
}

function deriveService(name: string, appUuid: string): string {
  const idx = name.indexOf(`-${appUuid}`);
  return idx > 0 ? name.slice(0, idx) : name;
}

function parseHealth(status: string): ComposeContainer['health'] {
  if (/\(healthy\)/i.test(status)) return 'healthy';
  if (/\(unhealthy\)/i.test(status)) return 'unhealthy';
  if (/\(health: starting\)/i.test(status) || /starting/i.test(status)) return 'starting';
  return 'none';
}

/**
 * List all docker containers belonging to a Coolify app (compose or
 * single-container). Returns an empty array when the deployment hasn't
 * started yet. Throws only on transport errors.
 */
export async function listContainersForApp(appUuid: string): Promise<ComposeContainer[]> {
  const res = await runOnCoolifyHost(
    `docker ps -a --filter ${sq('name=' + appUuid)} --format '{{.Names}}\t{{.Status}}'`,
    { timeoutMs: 8_000 },
  );
  if (res.code !== 0) {
    throw new Error(`docker ps exited ${res.code}: ${res.stderr.trim()}`);
  }
  return res.stdout
    .split('\n')
    .map(l => l.trim())
    .filter(Boolean)
    .map(l => {
      const [name, ...rest] = l.split('\t');
      const status = rest.join('\t');
      return {
        name,
        service: deriveService(name, appUuid),
        status,
        health: parseHealth(status),
      };
    });
}

/**
 * Resolve a single target container for an action that only makes
 * sense on one service (e.g. `apps.exec`). Behaviour:
 *   - No containers       → throws "no containers"
 *   - One container total → returns it (service param optional)
 *   - N containers and `service` given → match by derived service
 *   - N containers, no `service` → throws "must specify service"
 */
export async function resolveAppTargetContainer(
  appUuid: string,
  service: string | undefined,
): Promise<ComposeContainer> {
  const containers = await listContainersForApp(appUuid);
  if (containers.length === 0) {
    throw new Error(`No containers found for app ${appUuid}. Deploy may not have started.`);
  }
  if (containers.length === 1 && !service) {
    return containers[0];
  }
  if (!service) {
    throw new Error(
      `App has ${containers.length} containers; specify service. Available: ${containers.map(c => c.service).join(', ')}`,
    );
  }
  const match = containers.find(c => c.service === service);
  if (!match) {
    throw new Error(
      `Service "${service}" not found. Available: ${containers.map(c => c.service).join(', ')}`,
    );
  }
  return match;
}
