/**
 * Per-tool sliding-window rate limiter.
 *
 * Each tool call is recorded in `tool_rate_limit_events`. Before each call
 * the registry checks whether the agent (main or sub) has exceeded the tool's
 * window limit. If so, the call is blocked and the caller receives
 * { ok: false, status: 'rate_limited', retry_after_seconds, current_count, max }.
 *
 * Scoping rules:
 *   - Main agent calls are stored with agent_id = NULL.
 *   - Sub-agent calls are stored with agent_id = <agent_id string>.
 *   - The two are independent counters — one does not drain the other's quota.
 *
 * API:
 *   getLimitFor(tool)           — reads tool.rate_limit; returns { window_seconds, max } or null.
 *   checkLimit(toolName, agentId) — returns { ok: true } or { ok: false, ... }.
 *   recordCall(toolName, agentId) — inserts one event row after successful execution.
 *   gcOldEvents()               — deletes rows older than 24 h (called by heartbeat GC task).
 *
 * Note: this rate limiter is distinct from the WhatsApp router's 5-msg/min
 * per-sender cap (lib/whatsapp-router.js). That one is sender-scoped; this
 * one is tool-scoped. Both exist independently and are not connected.
 */

import { getDb, nowIso, newId } from './db.js';

/**
 * Return the rate-limit config from a tool definition object.
 * Returns null when the tool has no rate_limit field (unlimited).
 *
 * @param {{ rate_limit?: { window_seconds: number, max: number } }} tool
 * @returns {{ window_seconds: number, max: number } | null}
 */
export function getLimitFor(tool) {
  if (!tool || !tool.rate_limit) return null;
  const { window_seconds, max } = tool.rate_limit;
  if (typeof window_seconds !== 'number' || typeof max !== 'number') return null;
  return { window_seconds, max };
}

/**
 * Check whether an agent is under quota for a tool.
 *
 * Looks up the window config directly from tool_rate_limit_events.
 * Callers (registry.js) pass the MARKETPLACE_TOOLS entry via `tool` when
 * they have it; this function only needs the name + a limit config.
 *
 * @param {string} toolName
 * @param {string|null|undefined} agentId  — null / undefined = main agent
 * @param {{ window_seconds: number, max: number }} limit  — from getLimitFor(tool)
 * @returns {{ ok: true } | { ok: false, retry_after_seconds: number, current_count: number, max: number }}
 */
function checkLimitWith(toolName, agentId, limit) {
  const db = getDb();
  const windowStart = new Date(Date.now() - limit.window_seconds * 1000).toISOString();

  // Scope: main agent (agentId falsy) counts rows where agent_id IS NULL.
  //        sub-agent counts rows where agent_id = <value>.
  let count;
  if (!agentId) {
    count = db.prepare(
      'SELECT COUNT(*) AS cnt FROM tool_rate_limit_events WHERE tool_name = ? AND agent_id IS NULL AND at >= ?'
    ).get(toolName, windowStart).cnt;
  } else {
    count = db.prepare(
      'SELECT COUNT(*) AS cnt FROM tool_rate_limit_events WHERE tool_name = ? AND agent_id = ? AND at >= ?'
    ).get(toolName, agentId, windowStart).cnt;
  }

  if (count >= limit.max) {
    // Find the oldest event in the window to estimate when capacity next opens.
    let oldestAt;
    if (!agentId) {
      const row = db.prepare(
        'SELECT at FROM tool_rate_limit_events WHERE tool_name = ? AND agent_id IS NULL AND at >= ? ORDER BY at ASC LIMIT 1'
      ).get(toolName, windowStart);
      oldestAt = row?.at;
    } else {
      const row = db.prepare(
        'SELECT at FROM tool_rate_limit_events WHERE tool_name = ? AND agent_id = ? AND at >= ? ORDER BY at ASC LIMIT 1'
      ).get(toolName, agentId, windowStart);
      oldestAt = row?.at;
    }

    let retryAfterSeconds = limit.window_seconds;
    if (oldestAt) {
      const windowExpiresMs = new Date(oldestAt).getTime() + limit.window_seconds * 1000;
      retryAfterSeconds = Math.max(1, Math.ceil((windowExpiresMs - Date.now()) / 1000));
    }

    return {
      ok: false,
      retry_after_seconds: retryAfterSeconds,
      current_count: count,
      max: limit.max,
    };
  }

  return { ok: true };
}

/**
 * Check whether an agent is under quota for a tool.
 * Pass the MARKETPLACE_TOOLS entry as `tool` so the limit config is available.
 *
 * @param {string} toolName
 * @param {string|null|undefined} agentId
 * @param {{ rate_limit?: { window_seconds: number, max: number } }} tool  — tool definition
 * @returns {{ ok: true } | { ok: false, retry_after_seconds: number, current_count: number, max: number }}
 */
export function checkLimit(toolName, agentId, tool) {
  const limit = getLimitFor(tool);
  if (!limit) return { ok: true };
  return checkLimitWith(toolName, agentId, limit);
}

/**
 * Record a successful tool call.
 * Only called AFTER the adapter returns successfully — failures do not
 * consume quota.
 *
 * @param {string} toolName
 * @param {string|null|undefined} agentId
 * @param {{ rate_limit?: { window_seconds: number, max: number } }} tool  — tool definition
 */
export function recordCall(toolName, agentId, tool) {
  const limit = getLimitFor(tool);
  if (!limit) return; // No limit defined for this tool — nothing to record.

  getDb().prepare(
    'INSERT INTO tool_rate_limit_events (id, tool_name, agent_id, at) VALUES (?, ?, ?, ?)'
  ).run(newId('rl'), toolName, agentId || null, nowIso());
}

/**
 * Delete events older than 24 h.
 * Called daily via heartbeat task type 'tool_rate_limit_gc'.
 *
 * @returns {{ deleted: number }}
 */
export function gcOldEvents() {
  const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
  const result = getDb().prepare(
    'DELETE FROM tool_rate_limit_events WHERE at < ?'
  ).run(cutoff);
  return { deleted: result.changes };
}
