#!/usr/bin/env -S npx --yes tsx
// =============================================================================
// session_wait_till_idle.ts — Block until busy session count drops below a threshold
//
// Part of: .opencode/skills/auto-agents-system/scripts/
//
// Polls the OpenCode Server and counts busy sessions matching one of two modes:
//
//   supervisors — counts all busy sessions whose tag ends in "-SUP"
//   workers     — counts all busy sessions whose tag starts with "{prefix}-"
//                 but does NOT end in "-SUP"
//
// The script blocks (sleeps + re-polls) while the busy count is >= the
// specified max pool size.  It unblocks when the count drops below the
// threshold OR the timeout is reached, whichever comes first.
//
// USAGE:
//   session_wait_till_idle.ts --mode (supervisors|workers) \
//     --max-pool-size N --timeout SECONDS \
//     [--tag-prefix PREFIX] [--server URL]
//
//   --mode MODE            "supervisors" or "workers"
//   --tag-prefix PREFIX    Tag prefix for worker matching (required when mode=workers,
//                          ignored when mode=supervisors)
//   --max-pool-size N      Positive integer; block while busy_count >= N
//   --timeout SECONDS      Positive integer; maximum seconds to block
//   --server URL           OpenCode Server base URL (default: http://localhost:4096)
//
// OUTPUT (stdout): JSON object with wait result details.
//
// EXIT CODE: Always 0.  The `timed_out` field in the JSON output indicates
//   whether the timeout was reached before the condition was satisfied.
//
// DEPENDENCIES: Node.js >= 18 (native fetch), npx tsx (TypeScript execution)
// =============================================================================
import { fileURLToPath } from 'node:url';
import {
  listSessions,
  getAllStatuses,
  withStatuses,
  parseServerFlag,
  getFlag,
  sleep,
  type SessionWithStatus,
} from './session_api.ts';

// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────

/** Polling interval between session list checks (milliseconds) */
const POLL_INTERVAL_MS = 5_000;

// ─────────────────────────────────────────────────────────────────────────────
// TYPES
// ─────────────────────────────────────────────────────────────────────────────

export type WaitMode = 'supervisors' | 'workers';

export interface WaitTillIdleOptions {
  server: string;
  mode: WaitMode;
  tagPrefix?: string;       // required when mode === 'workers'
  maxPoolSize: number;       // positive integer
  timeoutSeconds: number;    // positive integer
}

export interface WaitTillIdleResult {
  waited: boolean;           // true if the script had to block at least once
  timed_out: boolean;        // true if the timeout was reached before condition was met
  elapsed_seconds: number;   // wall-clock seconds spent waiting (rounded to 1 decimal)
  busy_count: number;        // final busy count at the moment the script returned
  max_pool_size: number;     // the threshold that was used
  mode: WaitMode;
  tag_prefix: string | null; // the tag prefix used (null for supervisors)
}

// ─────────────────────────────────────────────────────────────────────────────
// MATCHING HELPERS
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Count busy sessions that match the given mode criteria.
 *
 * Supervisors: tag ends with "-SUP"
 * Workers:     tag starts with "{tagPrefix}-" AND does NOT end with "-SUP"
 */
function countBusyMatches(
  sessions: SessionWithStatus[],
  mode: WaitMode,
  tagPrefix?: string,
): number {
  return sessions.filter(s => {
    if (s.status !== 'busy') return false;
    if (s.tag === null) return false;

    if (mode === 'supervisors') {
      return s.tag.endsWith('-SUP');
    }

    // mode === 'workers'
    return s.tag.startsWith(tagPrefix + '-') && !s.tag.endsWith('-SUP');
  }).length;
}

// ─────────────────────────────────────────────────────────────────────────────
// CORE WAIT LOGIC (exported for wrapper scripts)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Block until the number of busy sessions matching the mode criteria drops
 * below `maxPoolSize`, or until the timeout is reached.
 */
export async function waitTillIdle(opts: WaitTillIdleOptions): Promise<WaitTillIdleResult> {
  const { server, mode, tagPrefix, maxPoolSize, timeoutSeconds } = opts;
  const startMs = Date.now();
  const deadlineMs = startMs + timeoutSeconds * 1_000;

  let waited = false;
  let timedOut = false;
  let busyCount = 0;

  // eslint-disable-next-line no-constant-condition
  while (true) {
    // Fetch current session state
    const [sessions, statuses] = await Promise.all([
      listSessions(server),
      getAllStatuses(server),
    ]);
    const enriched = withStatuses(sessions, statuses);
    busyCount = countBusyMatches(enriched, mode, tagPrefix);

    const elapsedS = (Date.now() - startMs) / 1_000;
    const modeLabel = mode === 'supervisors'
      ? 'supervisor sessions (tag ends with -SUP)'
      : `worker sessions (tag starts with ${tagPrefix}-, excludes -SUP)`;

    process.stderr.write(
      `[${elapsedS.toFixed(1)}s] Busy ${modeLabel}: ${busyCount} / max ${maxPoolSize}\n`,
    );

    // Condition met: busy count is below the threshold
    if (busyCount < maxPoolSize) {
      break;
    }

    // Check timeout BEFORE sleeping — if we're already past deadline, exit
    if (Date.now() >= deadlineMs) {
      timedOut = true;
      break;
    }

    // Block: sleep then re-poll
    waited = true;

    // Sleep for the poll interval, but cap at remaining time to not overshoot
    const remainingMs = deadlineMs - Date.now();
    const sleepMs = Math.min(POLL_INTERVAL_MS, Math.max(0, remainingMs));
    if (sleepMs <= 0) {
      timedOut = true;
      break;
    }
    await sleep(sleepMs);

    // Re-check timeout after sleeping
    if (Date.now() >= deadlineMs) {
      // Do one final poll to get the latest count before declaring timeout
      const [finalSessions, finalStatuses] = await Promise.all([
        listSessions(server),
        getAllStatuses(server),
      ]);
      const finalEnriched = withStatuses(finalSessions, finalStatuses);
      busyCount = countBusyMatches(finalEnriched, mode, tagPrefix);

      // If condition is now met, it's not a timeout
      if (busyCount < maxPoolSize) {
        break;
      }
      timedOut = true;
      break;
    }
  }

  const elapsedSeconds = Math.round((Date.now() - startMs) / 100) / 10; // 1 decimal

  return {
    waited,
    timed_out: timedOut,
    elapsed_seconds: elapsedSeconds,
    busy_count: busyCount,
    max_pool_size: maxPoolSize,
    mode,
    tag_prefix: mode === 'workers' ? (tagPrefix ?? null) : null,
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// VALIDATION HELPERS
// ─────────────────────────────────────────────────────────────────────────────

/** Parse and validate a positive integer from a string. Returns null on failure. */
function parsePositiveInt(value: string | undefined, name: string): number | null {
  if (value === undefined) {
    process.stderr.write(`ERROR: ${name} is required\n`);
    return null;
  }
  const n = Number(value);
  if (!Number.isInteger(n) || n <= 0) {
    process.stderr.write(`ERROR: ${name} must be a positive integer, got "${value}"\n`);
    return null;
  }
  return n;
}

// ─────────────────────────────────────────────────────────────────────────────
// MAIN
// ─────────────────────────────────────────────────────────────────────────────

async function main(): Promise<void> {
  const { server, rest } = parseServerFlag(process.argv);

  if (rest.includes('-h') || rest.includes('--help')) {
    printUsage();
    process.exit(0);
  }

  // Parse mode
  const modeStr = getFlag(rest, '--mode');
  if (!modeStr || (modeStr !== 'supervisors' && modeStr !== 'workers')) {
    process.stderr.write('ERROR: --mode must be "supervisors" or "workers"\n');
    process.exit(1);
  }
  const mode: WaitMode = modeStr;

  // Parse tag prefix (required for workers)
  const tagPrefix = getFlag(rest, '--tag-prefix');
  if (mode === 'workers' && !tagPrefix) {
    process.stderr.write('ERROR: --tag-prefix is required when mode is "workers"\n');
    process.exit(1);
  }

  // Parse max pool size
  const maxPoolSize = parsePositiveInt(getFlag(rest, '--max-pool-size'), '--max-pool-size');
  if (maxPoolSize === null) process.exit(1);

  // Parse timeout
  const timeoutSeconds = parsePositiveInt(getFlag(rest, '--timeout'), '--timeout');
  if (timeoutSeconds === null) process.exit(1);

  process.stderr.write(
    `Waiting for ${mode} pool to have fewer than ${maxPoolSize} busy session(s)` +
    (mode === 'workers' ? ` (prefix: ${tagPrefix})` : '') +
    ` — timeout: ${timeoutSeconds}s\n`,
  );

  const result = await waitTillIdle({
    server,
    mode,
    tagPrefix: tagPrefix ?? undefined,
    maxPoolSize,
    timeoutSeconds,
  });

  if (result.timed_out) {
    process.stderr.write(
      `Timed out after ${result.elapsed_seconds}s — busy count (${result.busy_count}) ` +
      `still >= max pool size (${result.max_pool_size})\n`,
    );
  } else {
    process.stderr.write(
      `Condition met after ${result.elapsed_seconds}s — busy count: ${result.busy_count} ` +
      `< max pool size: ${result.max_pool_size}\n`,
    );
  }

  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
}

function printUsage(): void {
  process.stderr.write(`\
Usage:
  session_wait_till_idle.ts --mode (supervisors|workers) \\
    --max-pool-size N --timeout SECONDS \\
    [--tag-prefix PREFIX] [--server URL]

REQUIRED:
  --mode MODE            "supervisors" or "workers"
  --max-pool-size N      Positive integer; block while busy_count >= N
  --timeout SECONDS      Positive integer; maximum seconds to block

CONDITIONAL:
  --tag-prefix PREFIX    Tag prefix for worker matching (required for workers,
                         ignored for supervisors)

OPTIONAL:
  --server URL           OpenCode Server base URL (default: http://localhost:4096)
  -h, --help             Print this message and exit

MODES:
  supervisors   Count busy sessions whose tag ends in "-SUP".
                Blocks while that count >= max-pool-size.

  workers       Count busy sessions whose tag starts with "{prefix}-"
                but does NOT end in "-SUP".
                Blocks while that count >= max-pool-size.

OUTPUT:
  JSON object to stdout with fields:
    waited, timed_out, elapsed_seconds, busy_count, max_pool_size, mode, tag_prefix
`);
}

// ─────────────────────────────────────────────────────────────────────────────
// CLI ENTRY POINT
// ─────────────────────────────────────────────────────────────────────────────

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  main().catch((err: unknown) => {
    process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
    process.exit(1);
  });
}
