/**
 * services/keyRotator.js
 *
 * Multi-key shuffle + quota fallback for external services.
 * All external service calls MUST go through here — never use keys directly.
 *
 *   const key = await keyRotator.getKey('scrape_do');
 *   try { ...use key...; keyRotator.recordSuccess('scrape_do', key.id); }
 *   catch (e) { keyRotator.recordFailure('scrape_do', key.id, e); }
 *
 * Or use runWithRotation() for automatic fallback:
 *
 *   const result = await keyRotator.runWithRotation('scrape_do', async (key) => {
 *     return await scrapeUrl(url, key.key_value);
 *   });
 */

'use strict';

const crypto    = require('crypto');
const apiKey    = require('../models/apiKey');
// Note: email.js depends on this module. Use a lazy require to avoid
// returning an empty exports object during the circular import.
const { db }    = require('../config/db');
const alertBus  = require('./alertBus');

const EXHAUST_RE = /(quota|rate.?limit|429|too many|exhaust|limit reached)/i;

/**
 * Bootstrap from .env if api_keys table is empty for a service.
 */
function bootstrapFromEnv() {
  const seeds = [
    { service: 'scrape_do', env: 'SCRAPEDO_API_KEY', label: 'env:scrape_do', quota: parseInt(process.env.SCRAPEDO_DAILY_QUOTA, 10) || 950 },
    { service: 'MiniMax',   env: 'MiniMax_API_KEY',   label: 'env:MiniMax',  quota: parseInt(process.env.MiniMax_DAILY_QUOTA, 10) || 950 },
    { service: 'resend',    env: 'RESEND_API_KEY',    label: 'env:resend',   quota: parseInt(process.env.RESEND_DAILY_QUOTA, 10) || 95 },
  ];
  for (const s of seeds) {
    const v = process.env[s.env];
    if (!v) continue;
    const existing = apiKey.listForService(s.service);
    if (existing.length === 0) {
      try {
        apiKey.create({ service: s.service, key_value: v, label: s.label, quota_limit: s.quota });
        console.log(`[keyRotator] seeded ${s.service} from env`);
      } catch (e) {
        console.warn(`[keyRotator] failed to seed ${s.service}: ${e.message}`);
      }
    }
  }
}

function shuffle(arr) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = crypto.randomInt(0, i + 1);
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

/**
 * Pick a random active key for `service`. Returns null if none.
 */
function getKey(service) {
  const keys = apiKey.getActiveForService(service);
  if (!keys.length) return null;
  // Shuffle and pick first. Skip keys over their daily quota.
  const candidates = shuffle(keys).filter(k => k.calls_today < k.quota_limit);
  return candidates[0] || keys[0];
}

function recordSuccess(service, keyId) {
  if (keyId) apiKey.recordSuccess(keyId);
}

function recordFailure(service, keyId, err) {
  if (!keyId) return;
  const message = String(err && err.message || err);
  apiKey.recordFailure(keyId, message);
  if (EXHAUST_RE.test(message)) {
    apiKey.setExhausted(keyId, nextMidnight(), message);
  }
}

function nextMidnight() {
  const d = new Date();
  d.setUTCHours(24, 0, 0, 0);
  return d.getTime();
}

/**
 * Run `fn(key)` with rotation. Tries each key in random order; stops on first
 * non-quota error. Returns the result of fn, or throws the last error.
 */
async function runWithRotation(service, fn, { maxAttempts = null } = {}) {
  const keys = apiKey.getActiveForService(service);
  if (!keys.length) {
    throw new Error(`NO_KEYS_AVAILABLE:${service}`);
  }
  const order = shuffle(keys);
  const limit = maxAttempts || order.length;
  let lastErr = null;
  for (let i = 0; i < Math.min(limit, order.length); i++) {
    const k = order[i];
    if (k.calls_today >= k.quota_limit) {
      apiKey.setExhausted(k.id, nextMidnight(), 'pre-check quota exceeded');
      continue;
    }
    const start = Date.now();
    try {
      const out = await fn(k);
      apiKey.recordSuccess(k.id);
      return { result: out, key: k, responseTime: Date.now() - start };
    } catch (err) {
      lastErr = err;
      const msg = String(err && err.message || err);
      apiKey.recordFailure(k.id, msg);
      if (EXHAUST_RE.test(msg)) {
        apiKey.setExhausted(k.id, nextMidnight(), msg);
        alertBus.raise({
          severity: 'warn',
          category: 'api_quota',
          service,
          title: `${service} key exhausted (quota / 429)`,
          detail: { key_id: k.id, label: k.label, last4: k.key_value ? k.key_value.slice(-4) : '', error: msg, retry_after: nextMidnight() },
          dedupe_key: `quota:${service}:${k.id}`,
        });
        // The "Monthly request limit exceeded" message means the
        // whole Scrape.do ACCOUNT is at quota, not just this key.
        // Mark every other active key for this service as exhausted
        // too so we don't burn them on the next call. Then stop
        // trying — rethrow so the caller can abort the scan.
        if (/Monthly request limit|account/i.test(msg)) {
          for (const other of apiKey.getActiveForService(service)) {
            if (other.id !== k.id) apiKey.setExhausted(other.id, nextMidnight(), 'account-level quota exhausted');
          }
          const e = new Error(`ACCOUNT_QUOTA_EXHAUSTED:${service}`);
          e.code = 'ACCOUNT_QUOTA_EXHAUSTED';
          e.service = service;
          throw e;
        }
        // try next key
        continue;
      }
    // non-quota error: rethrow after alerting
    alertBus.raise({
      severity: 'error',
      category: 'api_error',
      service,
      title: `${service} call failed (non-quota)`,
      detail: { key_id: k.id, label: k.label, error: msg },
      dedupe_key: `error:${service}:${k.id}`,
    });
    throw err;
  }
  }
  throw lastErr || new Error(`ALL_KEYS_EXHAUSTED:${service}`);
}

/**
 * Return services whose active keys are all exhausted. Used by alert job.
 */
function exhaustedServices() {
  const services = apiKey.ROTATED_SERVICES;
  const out = [];
  for (const s of services) {
    const active = apiKey.getActiveForService(s);
    if (active.length === 0) {
      const all = apiKey.listForService(s);
      if (all.length > 0) out.push({ service: s, reason: 'all_exhausted', total: all.length });
      else                out.push({ service: s, reason: 'no_keys',        total: 0 });
    }
  }
  return out;
}

/**
 * Send admin alert email when all keys for a service are exhausted.
 */
async function alertIfExhausted() {
  const exhausted = exhaustedServices();
  if (!exhausted.length) return;
  const to = process.env.RESEND_ALERT_TO;
  if (!to) return;
  const subject = `[Shield] ${exhausted.length} service(s) out of keys`;
  const body = exhausted.map(e => `- ${e.service}: ${e.reason} (${e.total} key(s) on file)`).join('\n');
  try {
    // Lazy require to break circular dep with email.js
    const email = require('./email');
    await email.sendRaw(to, subject, body, { tag: 'system-alert' });
  } catch (e) {
    console.error('[keyRotator] failed to send exhaustion alert:', e.message);
  }
}

module.exports = {
  bootstrapFromEnv,
  getKey,
  recordSuccess,
  recordFailure,
  runWithRotation,
  exhaustedServices,
  alertIfExhausted,
  nextMidnight,
};
