// wall-e/llm/retry.js
// Ported from OpenCode (https://github.com/anomalyco/opencode)
// Source: packages/opencode/src/session/retry.ts
// Adapted from Effect-TS to plain Node.js async/await.
'use strict';

const RETRY_INITIAL_DELAY = 2000;
const RETRY_BACKOFF_FACTOR = 2;
const RETRY_MAX_DELAY_NO_HEADERS = 30_000; // 30 seconds
const MAX_RETRIES = 5;

/**
 * Parse retry delay from response headers or fall back to exponential backoff.
 * Priority: retry-after-ms > retry-after (seconds) > retry-after (HTTP date) > backoff
 * @param {object|null} response - Fetch response (or null)
 * @param {number} attempt - Zero-based attempt number
 * @returns {number} Delay in milliseconds
 */
function parseRetryDelay(response, attempt) {
  if (response && response.headers) {
    const get = typeof response.headers.get === 'function'
      ? (k) => response.headers.get(k)
      : (k) => response.headers[k];

    // 1. retry-after-ms (milliseconds, Anthropic-specific)
    const retryAfterMs = get('retry-after-ms');
    if (retryAfterMs) {
      const parsed = parseFloat(retryAfterMs);
      if (!isNaN(parsed)) return Math.min(parsed, RETRY_MAX_DELAY_NO_HEADERS);
    }

    // 2. retry-after (seconds or HTTP date, standard)
    const retryAfter = get('retry-after');
    if (retryAfter) {
      const seconds = parseFloat(retryAfter);
      if (!isNaN(seconds)) return Math.min(seconds * 1000, RETRY_MAX_DELAY_NO_HEADERS);
      const dateMs = Date.parse(retryAfter) - Date.now();
      if (!isNaN(dateMs) && dateMs > 0) return Math.min(dateMs, RETRY_MAX_DELAY_NO_HEADERS);
    }
  }

  // 3. Exponential backoff: 2s * 2^attempt, capped at 30s
  return Math.min(
    RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt),
    RETRY_MAX_DELAY_NO_HEADERS,
  );
}

// Patterns that indicate context overflow (non-retryable)
const CONTEXT_OVERFLOW_PATTERNS = ['context length exceeded', 'context_length_exceeded', 'maximum context'];

/**
 * Classify whether an HTTP error is retryable.
 * @param {number} status - HTTP status code
 * @param {string} body - Response body text
 * @returns {boolean}
 */
function isRetryable(status, body) {
  // Context overflow -- never retry
  if (typeof body === 'string') {
    const lower = body.toLowerCase();
    for (const pat of CONTEXT_OVERFLOW_PATTERNS) {
      if (lower.includes(pat)) return false;
    }
  }

  const statusText = String(status || '').toUpperCase();
  if (new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND', 'ECONNABORTED']).has(statusText)) {
    return true;
  }

  // Retryable status codes
  if (status === 429) return true;  // rate limited
  if (status === 529) return true;  // overloaded (Anthropic)
  if (status === 503) return true;  // service unavailable
  if (status >= 500 && status < 600) return true;  // other server errors

  // Check body for rate limit / overloaded patterns
  if (typeof body === 'string') {
    const lower = body.toLowerCase();
    if (/\b(?:econnreset|econnrefused|etimedout|eai_again|enotfound|econnaborted)\b/.test(lower) ||
        lower.includes('socket hang up') ||
        lower.includes('connection reset') ||
        lower.includes('network timeout') ||
        lower.includes('fetch failed') ||
        lower === 'terminated' ||
        lower.includes('\nterminated')) {
      return true;
    }
    if (lower.includes('rate limit') || lower.includes('overloaded') ||
        lower.includes('too many requests')) return true;

    // Check JSON body for structured error patterns
    try {
      const json = JSON.parse(body);
      if (json.type === 'error' && json.error?.type === 'too_many_requests') return true;
      const code = typeof json.code === 'string' ? json.code : '';
      if (code.includes('exhausted') || code.includes('unavailable')) return true;
      if (json.type === 'error' && typeof json.error?.code === 'string' &&
          json.error.code.includes('rate_limit')) return true;
    } catch { /* not JSON */ }
  }

  return false;
}

/**
 * Fetch with automatic retry on transient errors.
 * Drop-in replacement for fetch() in LLM provider clients.
 * @param {string} url
 * @param {object} options - fetch options
 * @param {object} [retryOpts]
 * @param {number} [retryOpts.maxRetries=5]
 * @param {Function} [retryOpts._fetch] - Override fetch (for testing)
 * @param {number|null} [retryOpts._delayMs] - Override delay (for testing; 1 = near-instant)
 * @param {Function} [retryOpts.onRetry] - Callback(attempt, delay, status)
 * @returns {Promise<Response>}
 */
async function fetchWithRetry(url, options, retryOpts = {}) {
  const maxRetries = retryOpts.maxRetries ?? MAX_RETRIES;
  const doFetch = retryOpts._fetch || globalThis.fetch;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await doFetch(url, options);
    if (response.ok) return response;

    const body = await response.text();
    if (!isRetryable(response.status, body) || attempt === maxRetries) {
      const err = new Error(`API error ${response.status}: ${body.slice(0, 500)}`);
      err.status = response.status;
      err.responseBody = body;
      throw err;
    }

    const delay = retryOpts._delayMs != null
      ? retryOpts._delayMs
      : parseRetryDelay(response, attempt);
    if (retryOpts.onRetry) retryOpts.onRetry(attempt, delay, response.status);
    await new Promise(resolve => setTimeout(resolve, delay));
  }
}

/**
 * Wrap a provider.chat() call with retry logic.
 * Works with any provider (anthropic, openai, google, ollama).
 * @param {Function} chatFn - async () => response (the actual chat call)
 * @param {number} [maxRetries=3]
 * @returns {Promise<object>} The chat response
 */
async function withRetry(chatFn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await chatFn();
    } catch (err) {
      const status = err.status || err.code || 0;
      const message = err.message || '';
      if (attempt < maxRetries && isRetryable(status, message)) {
        const delay = parseRetryDelay(null, attempt);
        console.log(`[retry] Attempt ${attempt + 1}/${maxRetries} failed, retrying in ${delay}ms: ${message.slice(0, 100)}`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      throw err;
    }
  }
}

module.exports = {
  parseRetryDelay,
  isRetryable,
  fetchWithRetry,
  withRetry,
  RETRY_INITIAL_DELAY,
  RETRY_BACKOFF_FACTOR,
  RETRY_MAX_DELAY_NO_HEADERS,
  MAX_RETRIES,
};
