'use strict';

const DEFAULT_WALLE_READY_RETRY_DELAYS_MS = Object.freeze([750, 1500, 3000, 5000, 8000]);
const READY_ERROR_CODES = new Set([
  'ECONNREFUSED',
  'ECONNRESET',
  'EHOSTUNREACH',
  'ENETUNREACH',
  'ETIMEDOUT',
  'EPIPE',
  'WALLE_UNAVAILABLE',
]);

function parseRetryDelays(value, fallback = DEFAULT_WALLE_READY_RETRY_DELAYS_MS) {
  if (Array.isArray(value)) {
    const parsed = value.map((v) => Number(v)).filter((v) => Number.isFinite(v) && v >= 0);
    return parsed.length ? parsed : Array.from(fallback);
  }
  const raw = String(value || '').trim();
  if (!raw) return Array.from(fallback);
  const parsed = raw
    .split(',')
    .map((part) => Number(part.trim()))
    .filter((v) => Number.isFinite(v) && v >= 0);
  return parsed.length ? parsed : Array.from(fallback);
}

function isAbortLikeError(err) {
  const msg = String(err?.message || '');
  return err?.name === 'AbortError' || msg === 'Cancelled' || /aborted/i.test(msg);
}

function isRetriableWalleReadyError(err) {
  if (!err || err.providerError || err.code === 'AI_PROVIDER_ERROR') return false;
  if (err.walleStreamStarted || isAbortLikeError(err)) return false;
  if (READY_ERROR_CODES.has(err.code)) return true;
  if ([502, 503, 504].includes(Number(err.status))) return true;
  const msg = String(err.message || '');
  return /ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|ETIMEDOUT|EPIPE|socket hang up|fetch failed/i.test(msg);
}

function createWalleUnavailableError(message, details = {}) {
  const err = new Error(message || 'Wall-E is not ready');
  err.code = details.code || 'WALLE_UNAVAILABLE';
  if (details.status) err.status = details.status;
  if (details.body) err.responseBody = details.body;
  return err;
}

function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) return reject(new Error('Cancelled'));
    let timer = null;
    const onAbort = () => {
      clearTimeout(timer);
      reject(new Error('Cancelled'));
    };
    timer = setTimeout(() => {
      if (signal) signal.removeEventListener('abort', onAbort);
      resolve();
    }, Math.max(0, ms));
    if (signal) signal.addEventListener('abort', onAbort, { once: true });
  });
}

async function retryWalleReadyRequest(operation, options = {}) {
  const delaysMs = parseRetryDelays(options.delaysMs, DEFAULT_WALLE_READY_RETRY_DELAYS_MS);
  const maxAttempts = delaysMs.length + 1;
  const start = Date.now();
  let attempt = 0;
  let lastErr = null;

  while (attempt < maxAttempts) {
    attempt += 1;
    try {
      return await operation({ attempt, maxAttempts });
    } catch (err) {
      lastErr = err;
      const retriable = typeof options.shouldRetry === 'function'
        ? options.shouldRetry(err, { attempt, maxAttempts })
        : isRetriableWalleReadyError(err);
      if (!retriable || attempt >= maxAttempts) break;

      const delayMs = delaysMs[attempt - 1] || 0;
      if (typeof options.onRetry === 'function') {
        await options.onRetry({
          error: err,
          attempt,
          nextAttempt: attempt + 1,
          maxAttempts,
          delayMs,
          elapsedMs: Date.now() - start,
        });
      }
      await sleep(delayMs, options.signal);
    }
  }

  if (lastErr && isRetriableWalleReadyError(lastErr)) {
    lastErr.walleReadyRetryExhausted = true;
    lastErr.walleReadyAttempts = attempt;
    lastErr.walleReadyElapsedMs = Date.now() - start;
  }
  throw lastErr;
}

module.exports = {
  DEFAULT_WALLE_READY_RETRY_DELAYS_MS,
  createWalleUnavailableError,
  isRetriableWalleReadyError,
  parseRetryDelays,
  retryWalleReadyRequest,
};
