/**
 * Rate limit store — sliding window counter with datastore backing.
 *
 * Default: datastore-backed for shared enforcement across service instances.
 * Fallback: in-memory when RATE_LIMIT_STORE_MODE=memory.
 */

import crypto from 'node:crypto';
import { getCollection } from '../../lib/datastore.js';
import type { RateLimitEntry, RateLimitResult, RateLimitRule } from './types.js';

interface RateLimitDoc {
  id: string;
  productId: string;
  key: string;
  timestamps: number[];
  updatedAt: string;
  ttl?: number;
}

const memoryStore = new Map<string, RateLimitEntry>();
let storeNamespace = 'default';

function shouldUseMemoryStore(): boolean {
  return process.env.RATE_LIMIT_STORE_MODE === 'memory';
}

function rateLimitCollection() {
  return getCollection<RateLimitDoc>('rate_limit_entries', '/id');
}

function normalizeKey(key: string): string {
  return `${storeNamespace}:${key}`;
}

function docIdForKey(key: string): string {
  return `rl_${crypto.createHash('sha256').update(normalizeKey(key)).digest('hex')}`;
}

function toResult(
  timestamps: number[],
  rule: RateLimitRule,
  now: number,
  blocked: boolean
): RateLimitResult {
  const windowMs = rule.windowSeconds * 1000;

  if (blocked) {
    const oldestInWindow = timestamps[0];
    const retryAfterMs = oldestInWindow + windowMs - now;
    return {
      allowed: false,
      limit: rule.maxRequests,
      remaining: 0,
      resetAt: new Date(oldestInWindow + windowMs).toISOString(),
      retryAfterMs: Math.max(0, retryAfterMs),
    };
  }

  return {
    allowed: true,
    limit: rule.maxRequests,
    remaining: Math.max(0, rule.maxRequests - timestamps.length),
    resetAt: new Date(now + windowMs).toISOString(),
  };
}

async function checkAndRecordDatastore(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
  const now = Date.now();
  const windowMs = rule.windowSeconds * 1000;
  const windowStart = now - windowMs;
  const docId = docIdForKey(key);
  const existing = await rateLimitCollection().findById(docId, docId);
  const timestamps = (existing?.timestamps ?? []).filter(timestamp => timestamp > windowStart);

  if (timestamps.length >= rule.maxRequests) {
    return toResult(timestamps, rule, now, true);
  }

  const nextTimestamps = [...timestamps, now];
  const doc: RateLimitDoc = {
    id: docId,
    productId: '__platform__',
    key: normalizeKey(key),
    timestamps: nextTimestamps,
    updatedAt: new Date(now).toISOString(),
    ttl: Math.max(rule.windowSeconds * 2, 60),
  };

  await rateLimitCollection().upsert(doc);
  return toResult(nextTimestamps, rule, now, false);
}

function checkAndRecordMemory(key: string, rule: RateLimitRule): RateLimitResult {
  const now = Date.now();
  const windowMs = rule.windowSeconds * 1000;
  const windowStart = now - windowMs;
  const normalizedKey = normalizeKey(key);

  let entry = memoryStore.get(normalizedKey);
  if (!entry) {
    entry = { timestamps: [] };
    memoryStore.set(normalizedKey, entry);
  }

  entry.timestamps = entry.timestamps.filter(timestamp => timestamp > windowStart);
  if (entry.timestamps.length >= rule.maxRequests) {
    return toResult(entry.timestamps, rule, now, true);
  }

  entry.timestamps.push(now);
  return toResult(entry.timestamps, rule, now, false);
}

async function peekDatastore(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
  const now = Date.now();
  const windowMs = rule.windowSeconds * 1000;
  const windowStart = now - windowMs;
  const docId = docIdForKey(key);
  const existing = await rateLimitCollection().findById(docId, docId);
  const timestamps = (existing?.timestamps ?? []).filter(timestamp => timestamp > windowStart);
  return toResult(timestamps, rule, now, timestamps.length >= rule.maxRequests);
}

function peekMemory(key: string, rule: RateLimitRule): RateLimitResult {
  const now = Date.now();
  const windowMs = rule.windowSeconds * 1000;
  const windowStart = now - windowMs;
  const entry = memoryStore.get(normalizeKey(key));
  const timestamps = (entry?.timestamps ?? []).filter(timestamp => timestamp > windowStart);
  return toResult(timestamps, rule, now, timestamps.length >= rule.maxRequests);
}

export async function checkAndRecord(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
  if (shouldUseMemoryStore()) {
    return checkAndRecordMemory(key, rule);
  }

  return checkAndRecordDatastore(key, rule);
}

export async function reset(key: string): Promise<void> {
  const normalizedKey = normalizeKey(key);
  if (shouldUseMemoryStore()) {
    memoryStore.delete(normalizedKey);
    return;
  }

  const docId = docIdForKey(key);
  await rateLimitCollection()
    .delete(docId, docId)
    .catch(() => {});
}

export async function peek(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
  if (shouldUseMemoryStore()) {
    return peekMemory(key, rule);
  }

  return peekDatastore(key, rule);
}

export async function clearAll(): Promise<void> {
  memoryStore.clear();
  storeNamespace = crypto.randomUUID();
}
