'use strict';
/**
 * API key rotation across multiple auth profiles per type.
 *
 * Item B of error-recovery deep-dive. Borrowed from OpenClaw's
 * `executeWithApiKeyRotation` (src/agents/api-key-rotation.ts):
 *   - dedupe a list of credential candidates
 *   - try each in turn
 *   - retry on rate-limit errors only (NOT on generic 5xx — those are
 *     server-side and rotating keys won't help)
 *   - throw the LAST error if all candidates fail
 *
 * Wall-E's adaptation leverages auth_profiles (Item C of multi-agent
 * deep-dive) + the cooldown state from Item C of this branch:
 *
 *   1. Look up all profiles with the requested `type` (e.g. 'anthropic')
 *   2. Filter out profiles currently on cooldown (from auth/cooldown.js)
 *   3. Walk the remaining profiles, calling `execute(credentials)` for each
 *   4. On rate-limit failure: markFailure (sets cooldown), try next
 *   5. On success: markSuccess (clears cooldown), return result
 *   6. On non-rate-limit error: throw immediately (rotation won't help)
 *   7. If all profiles cool, fall back to the user-provided primary
 *      credential (the global env-var path) so the system stays usable
 *      even when every profile is rate-limited
 *
 * Performance choice: rotation is OPT-IN by callers. If the user has
 * one Anthropic key, rotation is a no-op — the loop runs once. The
 * value kicks in when 2+ keys exist.
 */

const recoveryLogger = require('../lib/recovery-logger');
const cooldownModule = require('./cooldown');

/**
 * Recognize provider rate-limit errors.
 *
 * Coverage:
 *   - HTTP 429 (Too Many Requests) — universal
 *   - Anthropic: error.code === 'rate_limit_error' or 'rate_limit_exceeded'
 *   - OpenAI: error.code === 'rate_limit_exceeded'
 *   - Google Gemini: status='RESOURCE_EXHAUSTED' or 'QUOTA_EXCEEDED' string match
 *   - Generic body containing 'rate limit', 'too many requests', 'quota exceeded'
 *
 * Conservative on purpose — we DON'T treat generic 5xx as rate-limit
 * (those are server-side, retrying the same key on 5xx via withRetry
 * is the right move; rotating the key is wrong).
 */
function isRateLimitError(err) {
  if (!err) return false;
  const status = err.status ?? err.statusCode ?? 0;
  if (status === 429) return true;
  const msg = (err.message || err.body || '').toLowerCase();
  if (msg.includes('rate limit')
      || msg.includes('too many requests')
      || msg.includes('quota exceeded')
      || msg.includes('resource_exhausted')) {
    return true;
  }
  // Anthropic / OpenAI structured error codes
  const code = err.code || err.error?.code || err.error?.type;
  if (typeof code === 'string') {
    if (code.includes('rate_limit') || code.includes('quota')) return true;
  }
  return false;
}

/**
 * Execute `task` with the first available credential profile of the
 * given type, rotating to subsequent profiles on rate-limit failure.
 *
 * @param {Object} opts
 * @param {string} opts.type - profile type ('anthropic', 'openai', 'slack', etc.)
 * @param {Function} opts.execute - async (credentials, profile) => result
 * @param {Object} [opts.brain] - brain module (defaults to require)
 * @param {Object} [opts.fallbackCredentials] - object passed to execute
 *   when no profile is eligible (after all are cooled down or none exist).
 *   Use this for the global env-var path so rotation never makes the
 *   system strictly worse.
 * @returns {Promise<*>} result of execute(...)
 */
async function executeWithKeyRotation({ type, execute, brain, fallbackCredentials }) {
  if (typeof execute !== 'function') {
    throw new Error('executeWithKeyRotation requires execute(credentials, profile)');
  }
  const b = brain || require('../brain');

  let allProfiles = [];
  try {
    allProfiles = b.listAuthProfiles ? b.listAuthProfiles({ type }) : [];
  } catch { allProfiles = []; }

  // Filter out cooled-down profiles
  const eligible = [];
  const cooledDown = [];
  for (const p of allProfiles) {
    const cd = cooldownModule.isOnCooldown({ profileId: p.id, brain: b });
    if (cd) cooledDown.push({ profile: p, cooldown: cd });
    else eligible.push(p);
  }

  if (cooledDown.length > 0) {
    try {
      recoveryLogger.info('key-rotation', 'profiles_skipped_cooldown', {
        details: {
          type,
          eligible_count: eligible.length,
          skipped_count: cooledDown.length,
          skipped_profiles: cooledDown.map(({ profile, cooldown }) => ({
            name: profile.name,
            remaining_ms: cooldown.remaining_ms,
            consecutive_failures: cooldown.consecutive_failures,
          })),
        },
      });
    } catch {}
  }

  // If no eligible profiles + caller provided fallback, attempt fallback once.
  if (eligible.length === 0) {
    if (!fallbackCredentials) {
      throw new Error(`No eligible auth profiles for type "${type}" (${cooledDown.length} on cooldown, 0 available, no fallback).`);
    }
    try {
      recoveryLogger.info('key-rotation', 'fallback_to_globals', {
        details: { type, reason: cooledDown.length > 0 ? 'all_profiles_cooled' : 'no_profiles_configured' },
      });
    } catch {}
    return await execute(fallbackCredentials, null);
  }

  // Walk eligible profiles
  let lastError = null;
  const attempts = [];
  for (let i = 0; i < eligible.length; i++) {
    const profile = eligible[i];
    let credentials = null;
    try {
      const raw = b.getKv(profile.credentials_kv_key);
      credentials = raw ? JSON.parse(raw) : null;
    } catch {
      credentials = null;
    }
    if (!credentials) {
      // Skip profiles whose kv key is missing/empty — log it but don't
      // mark as failure (it's a config issue, not a runtime one).
      attempts.push({ profile: profile.name, status: 'skipped', reason: 'missing_credentials' });
      try {
        recoveryLogger.warn('key-rotation', 'profile_skipped_no_credentials', {
          profile: profile.name,
          details: { kv_key: profile.credentials_kv_key },
        });
      } catch {}
      continue;
    }

    try {
      const result = await execute(credentials, profile);
      cooldownModule.markSuccess({ profileId: profile.id, profileName: profile.name, brain: b });
      attempts.push({ profile: profile.name, status: 'success', attempt_index: i });
      try {
        recoveryLogger.info('key-rotation', 'execute_success', {
          profile: profile.name,
          details: { type, attempt_index: i, total_eligible: eligible.length },
        });
      } catch {}
      return result;
    } catch (err) {
      lastError = err;
      const rateLimited = isRateLimitError(err);
      attempts.push({
        profile: profile.name,
        status: 'failure',
        attempt_index: i,
        rate_limited: rateLimited,
        error: err.message,
      });

      try {
        recoveryLogger.warn('key-rotation', rateLimited ? 'rate_limit_rotated' : 'execute_failure', {
          profile: profile.name,
          details: {
            type,
            attempt_index: i,
            total_eligible: eligible.length,
            error: (err.message || '').slice(0, 200),
            rate_limited: rateLimited,
          },
        });
      } catch {}

      if (rateLimited) {
        cooldownModule.markFailure({
          profileId: profile.id,
          profileName: profile.name,
          reason: 'rate_limit',
          brain: b,
        });
        // Continue to next profile
      } else {
        // Non-rate-limit error: rotating won't help — bail with this error
        throw err;
      }
    }
  }

  // All eligible profiles rate-limited.
  if (fallbackCredentials) {
    try {
      recoveryLogger.warn('key-rotation', 'fallback_after_all_rate_limited', {
        details: {
          type,
          attempts: attempts.length,
        },
      });
    } catch {}
    return await execute(fallbackCredentials, null);
  }

  const err = new Error(
    `All ${eligible.length} eligible profile(s) for type "${type}" hit rate limit. `
    + `Last error: ${lastError?.message || 'unknown'}`
  );
  err.attempts = attempts;
  err.cause = lastError;
  throw err;
}

module.exports = {
  executeWithKeyRotation,
  isRateLimitError,
};
