/**
 * smoke-http/execute.ts — Run probes with retry/back-off, return a report.
 *
 * Uses native Node 18+ fetch — no external HTTP lib. Probes execute with
 * bounded concurrency. Timeout enforced per-attempt via AbortController.
 * Retry policy: immediate for attempts 1-2, exponential back-off from 3+.
 */
import type {
  Probe,
  ProbeResult,
  SmokeHttpArgs,
  SmokeHttpReport,
} from './types.js';

// ─── Single probe with retry ──────────────────────────────────────────────

async function probeOnce(
  probe: Probe,
  timeoutMs: number,
  followRedirects: boolean,
): Promise<{ status: number | null; error?: string; redirectChain: string[] }> {
  const redirectChain: string[] = [];
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(probe.url, {
      method: probe.method,
      signal: controller.signal,
      redirect: followRedirects ? 'follow' : 'manual',
    });
    if (res.redirected) {
      redirectChain.push(res.url);
    }
    return { status: res.status, redirectChain };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return { status: null, error: msg, redirectChain };
  } finally {
    clearTimeout(timer);
  }
}

async function probeWithRetry(
  probe: Probe,
  args: SmokeHttpArgs,
): Promise<ProbeResult> {
  const start = Date.now();
  let attempts = 0;
  let lastStatus: number | null = null;
  let lastError: string | undefined;
  let redirectChain: string[] = [];

  while (attempts < args.retries) {
    attempts++;
    const result = await probeOnce(probe, args.timeoutMs, args.followRedirects);
    lastStatus = result.status;
    lastError = result.error;
    redirectChain = result.redirectChain;

    if (lastStatus === probe.expect) {
      return {
        url: probe.url,
        label: probe.label ?? null,
        method: probe.method,
        expect: probe.expect,
        actual: lastStatus,
        attempts,
        durationMs: Date.now() - start,
        status: 'pass',
        redirectChain: redirectChain.length > 0 ? redirectChain : undefined,
      };
    }

    // Back-off: immediate for attempts 1-2, exponential from 3+.
    if (attempts < args.retries) {
      const base = args.retryDelayMs;
      const delay = attempts <= 2 ? base : Math.min(base * Math.pow(2, attempts - 2), 30_000);
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  return {
    url: probe.url,
    label: probe.label ?? null,
    method: probe.method,
    expect: probe.expect,
    actual: lastStatus,
    attempts,
    durationMs: Date.now() - start,
    status: 'fail',
    error: lastError ?? `expected ${probe.expect} got ${lastStatus ?? 'no response'}`,
    redirectChain: redirectChain.length > 0 ? redirectChain : undefined,
  };
}

// ─── Bounded concurrency runner ───────────────────────────────────────────

async function runWithConcurrency<T, R>(
  items: T[],
  concurrency: number,
  worker: (item: T) => Promise<R>,
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let next = 0;

  async function loop() {
    while (true) {
      const idx = next++;
      if (idx >= items.length) return;
      results[idx] = await worker(items[idx]);
    }
  }

  const loops = Array.from({ length: Math.min(concurrency, items.length) }, () => loop());
  await Promise.all(loops);
  return results;
}

// ─── Entry point ──────────────────────────────────────────────────────────

export async function execute(args: SmokeHttpArgs): Promise<SmokeHttpReport> {
  const results = await runWithConcurrency(
    args.probes,
    args.concurrency,
    (probe) => probeWithRetry(probe, args),
  );
  return { results };
}
