import "server-only";

import { and, eq, gte, lt, sql } from "drizzle-orm";

// DB imports are lazy so that pure unit tests of rateLimit() don't require
// DATABASE_URL or other app env. Only the *Async/pruneRateLimit/rateLimitInspect
// entry points touch the DB.
async function dbClient() {
  const [{ db }, schema] = await Promise.all([
    import("@/lib/db/client"),
    import("@/lib/db/schema"),
  ]);
  return { db, rateLimitEntries: schema.rateLimitEntries };
}

// Keep a small in-memory shim for tests and pre-DB codepaths. The production
// path uses the DB-backed implementation below, which survives restarts and
// is consistent across multiple app instances sharing one database.
type Bucket = { timestamps: number[] };
const memoryStore = new Map<string, Bucket>();

export type RateLimitOptions = {
  key: string;
  max: number;
  windowMs: number;
  now?: number;
};

export type RateLimitResult = {
  ok: boolean;
  remaining: number;
  resetAt: number;
};

/**
 * Synchronous, in-memory rate limit — used only by unit tests and for call
 * sites that cannot await. For production request throttling use
 * {@link rateLimitAsync}, which is DB-backed.
 */
export function rateLimit({
  key,
  max,
  windowMs,
  now = Date.now(),
}: RateLimitOptions): RateLimitResult {
  const cutoff = now - windowMs;
  const bucket = memoryStore.get(key) ?? { timestamps: [] };
  const recent = bucket.timestamps.filter((t) => t > cutoff);

  if (recent.length >= max) {
    memoryStore.set(key, { timestamps: recent });
    return { ok: false, remaining: 0, resetAt: recent[0] + windowMs };
  }
  recent.push(now);
  memoryStore.set(key, { timestamps: recent });
  return { ok: true, remaining: max - recent.length, resetAt: now + windowMs };
}

/**
 * DB-backed rate limit using fixed-size buckets keyed by (key, windowStart).
 * `windowStart` is quantised to the window size so concurrent requests in the
 * same window map to the same row. Failures are fail-open — we log and allow.
 */
export async function rateLimitAsync({
  key,
  max,
  windowMs,
  now = Date.now(),
}: RateLimitOptions): Promise<RateLimitResult> {
  const windowStartMs = Math.floor(now / windowMs) * windowMs;
  const windowStart = new Date(windowStartMs);
  const resetAt = windowStartMs + windowMs;
  try {
    const { db, rateLimitEntries } = await dbClient();
    const rows = await db
      .insert(rateLimitEntries)
      .values({ key, windowStart, hits: 1 })
      .onConflictDoUpdate({
        target: [rateLimitEntries.key, rateLimitEntries.windowStart],
        set: { hits: sql`${rateLimitEntries.hits} + 1` },
      })
      .returning({ hits: rateLimitEntries.hits });
    const hits = rows[0]?.hits ?? 1;
    if (hits > max) {
      return { ok: false, remaining: 0, resetAt };
    }
    return { ok: true, remaining: Math.max(0, max - hits), resetAt };
  } catch (err) {
    console.error("[rate-limit] DB error — failing open", err);
    return { ok: true, remaining: max, resetAt };
  }
}

/**
 * Periodic cleanup — callable from a cron endpoint. Deletes buckets older
 * than 24h (they're irrelevant once their window has passed).
 */
export async function pruneRateLimit(): Promise<number> {
  const { db, rateLimitEntries } = await dbClient();
  const cutoff = new Date(Date.now() - 24 * 60 * 60_000);
  const result = await db
    .delete(rateLimitEntries)
    .where(lt(rateLimitEntries.windowStart, cutoff))
    .returning({ key: rateLimitEntries.key });
  return result.length;
}

/** Inspect the current hit count for a key — admin system-health. */
export async function rateLimitInspect(key: string, windowMs: number) {
  const { db, rateLimitEntries } = await dbClient();
  const now = Date.now();
  const windowStart = new Date(Math.floor(now / windowMs) * windowMs);
  const [row] = await db
    .select({ hits: rateLimitEntries.hits })
    .from(rateLimitEntries)
    .where(
      and(
        eq(rateLimitEntries.key, key),
        gte(rateLimitEntries.windowStart, windowStart),
      ),
    )
    .limit(1);
  return { hits: row?.hits ?? 0, windowStart };
}

export function resetRateLimitForTests(): void {
  memoryStore.clear();
}

export function ipFromHeaders(headers: Headers): string {
  const xff = headers.get("x-forwarded-for");
  if (xff) return xff.split(",")[0].trim();
  const real = headers.get("x-real-ip");
  if (real) return real.trim();
  return "unknown";
}
