/**
 * Retry utility for external API calls.
 *
 * Features:
 * - Exponential backoff with jitter
 * - Respects Retry-After header (429)
 * - Permanent failure detection (400/401/403/404 → no retry)
 * - Per-request timeout via AbortController
 * - Domain-level rate limiting (e.g., Firecrawl 500ms gap)
 */

// ─── Types ───

export interface RetryOptions {
  /** Max number of retries (default: 2) */
  maxRetries?: number;
  /** Backoff delays in ms per attempt (default: [1000, 3000]) */
  backoffMs?: number[];
  /** HTTP status codes to retry on (default: [429, 500, 502, 503]) */
  retryOn?: number[];
  /** Per-request timeout in ms (default: 45000) */
  timeoutMs?: number;
  /** Label for logging */
  label?: string;
}

interface RetryResult {
  response: Response;
  attempts: number;
  retried: boolean;
}

// ─── Domain Rate Limiter ───

const domainLastCall = new Map<string, number>();
const DOMAIN_INTERVALS: Record<string, number> = {
  "api.firecrawl.dev": 500,
  "api.perplexity.ai": 200,
};

function getDomain(url: string): string {
  try {
    return new URL(url).hostname;
  } catch {
    return "";
  }
}

async function waitForDomainSlot(url: string): Promise<void> {
  const domain = getDomain(url);
  const interval = DOMAIN_INTERVALS[domain];
  if (!interval) return;

  const last = domainLastCall.get(domain) || 0;
  const elapsed = Date.now() - last;
  if (elapsed < interval) {
    await new Promise((r) => setTimeout(r, interval - elapsed));
  }
  domainLastCall.set(domain, Date.now());
}

// ─── Permanent failure codes (never retry) ───

const PERMANENT_FAILURES = new Set([400, 401, 403, 404, 405, 409, 422]);

// ─── Main Function ───

/**
 * fetch() with automatic retry, timeout, and rate limiting.
 *
 * @example
 * const res = await fetchWithRetry("https://api.example.com/data", {
 *   method: "POST",
 *   headers: { "Content-Type": "application/json" },
 *   body: JSON.stringify({ query: "test" }),
 * }, { maxRetries: 2, timeoutMs: 30000, label: "example-api" });
 */
export async function fetchWithRetry(
  url: string,
  init?: RequestInit,
  opts?: RetryOptions,
): Promise<Response> {
  const {
    maxRetries = 2,
    backoffMs = [1000, 3000],
    retryOn = [429, 500, 502, 503],
    timeoutMs = 45000,
    label = getDomain(url),
  } = opts || {};

  let lastError: Error | null = null;
  const retrySet = new Set(retryOn);

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    // Rate limit between domain calls
    await waitForDomainSlot(url);

    // AbortController for per-request timeout
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const response = await fetch(url, {
        ...init,
        signal: controller.signal,
      });
      clearTimeout(timer);

      // Success
      if (response.ok) return response;

      // Permanent failure — don't retry
      if (PERMANENT_FAILURES.has(response.status)) {
        console.warn(
          `[retry:${label}] Permanent failure ${response.status} on attempt ${attempt + 1}`,
        );
        return response;
      }

      // Retryable failure
      if (retrySet.has(response.status) && attempt < maxRetries) {
        let delay = backoffMs[attempt] || backoffMs[backoffMs.length - 1] || 3000;

        // Respect Retry-After header for 429
        if (response.status === 429) {
          const retryAfter = response.headers.get("Retry-After");
          if (retryAfter) {
            const parsed = parseInt(retryAfter, 10);
            if (!isNaN(parsed)) {
              delay = Math.max(delay, parsed * 1000);
            }
          }
        }

        // Add jitter (±20%)
        delay = delay * (0.8 + Math.random() * 0.4);

        console.warn(
          `[retry:${label}] Status ${response.status}, retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${maxRetries + 1})`,
        );
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }

      // Non-retryable or exhausted retries
      return response;
    } catch (err) {
      clearTimeout(timer);
      lastError = err instanceof Error ? err : new Error(String(err));

      if (attempt < maxRetries) {
        const delay = (backoffMs[attempt] || 3000) * (0.8 + Math.random() * 0.4);
        console.warn(
          `[retry:${label}] Network error: ${lastError.message}, retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${maxRetries + 1})`,
        );
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
    }
  }

  throw lastError || new Error(`[retry:${label}] All ${maxRetries + 1} attempts failed`);
}

// ─── Convenience: JSON fetch with retry ───

export async function fetchJsonWithRetry<T = unknown>(
  url: string,
  init?: RequestInit,
  opts?: RetryOptions,
): Promise<{ data: T | null; status: number; error?: string }> {
  try {
    const res = await fetchWithRetry(url, init, opts);
    if (!res.ok) {
      const text = await res.text().catch(() => "");
      return { data: null, status: res.status, error: `HTTP ${res.status}: ${text.slice(0, 200)}` };
    }
    const data = (await res.json()) as T;
    return { data, status: res.status };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    return { data: null, status: 0, error: msg };
  }
}

// ─── Channel Task Wrapper ───

export interface ChannelTaskResult {
  channel: string;
  success: boolean;
  error?: string;
  httpStatus?: number;
  durationMs: number;
}

/**
 * Wraps a channel collection task with timing and error capture.
 * Used by collect-channel-data to track per-channel success/failure.
 *
 * @example
 * const [result, taskMeta] = await wrapChannelTask("instagram", async () => {
 *   // ... collect instagram data ...
 *   channelData.instagram = data;
 * });
 */
export async function wrapChannelTask(
  channel: string,
  task: () => Promise<void>,
): Promise<ChannelTaskResult> {
  const start = Date.now();
  try {
    await task();
    return { channel, success: true, durationMs: Date.now() - start };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    console.error(`[channel:${channel}] Error: ${msg}`);
    return {
      channel,
      success: false,
      error: msg,
      durationMs: Date.now() - start,
    };
  }
}
