import type { Context, Next } from "hono";
import { config } from "./config.js";

const DEFAULT_MAX_REQUESTS = 100;
const DEFAULT_WINDOW_MS = 15_000;
const CLEANUP_INTERVAL_MS = 60_000;

interface WindowEntry {
  count: number;
  resetAt: number;
}

/**
 * Simple fixed-window in-memory rate limiter.
 *
 * Tracks request counts per IP within a configurable time window.
 * When the limit is exceeded, responds with 429 and a `Retry-After` header.
 * Periodically cleans up expired entries to prevent memory leaks.
 */
export class RateLimiter {
  private readonly maxRequests: number;
  private readonly windowMs: number;
  private readonly windows = new Map<string, WindowEntry>();
  private cleanupTimer: ReturnType<typeof setInterval> | null = null;

  constructor(maxRequests?: number, windowMs?: number) {
    this.maxRequests = maxRequests ?? DEFAULT_MAX_REQUESTS;
    this.windowMs = windowMs ?? DEFAULT_WINDOW_MS;
  }

  /**
   * Start the periodic cleanup timer that removes expired entries.
   * Call this once when the server starts.
   */
  startCleanup(intervalMs = CLEANUP_INTERVAL_MS): void {
    this.stopCleanup();
    this.cleanupTimer = setInterval(() => this.purgeExpired(), intervalMs);
    // Allow the process to exit even if the timer is still running
    if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
      this.cleanupTimer.unref();
    }
  }

  /** Stop the periodic cleanup timer. */
  stopCleanup(): void {
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer);
      this.cleanupTimer = null;
    }
  }

  /** Remove all entries whose window has expired. */
  purgeExpired(): void {
    const now = Date.now();
    for (const [key, entry] of this.windows) {
      if (now >= entry.resetAt) {
        this.windows.delete(key);
      }
    }
  }

  /**
   * Check whether a request from `key` (typically an IP) is allowed.
   * Returns an object indicating whether the request is allowed and
   * rate-limit metadata for response headers.
   */
  check(key: string): {
    allowed: boolean;
    remaining: number;
    resetAt: number;
    limit: number;
  } {
    const now = Date.now();
    const entry = this.windows.get(key);

    // No entry or window expired — start a new window
    if (!entry || now >= entry.resetAt) {
      const resetAt = now + this.windowMs;
      this.windows.set(key, { count: 1, resetAt });
      return { allowed: true, remaining: this.maxRequests - 1, resetAt, limit: this.maxRequests };
    }

    // Within the current window
    entry.count += 1;
    const remaining = Math.max(0, this.maxRequests - entry.count);
    const allowed = entry.count <= this.maxRequests;
    return { allowed, remaining, resetAt: entry.resetAt, limit: this.maxRequests };
  }

  /** Current number of tracked keys (for testing / monitoring). */
  get size(): number {
    return this.windows.size;
  }

  /** Reset all tracked state (primarily for testing). */
  reset(): void {
    this.windows.clear();
  }
}

/**
 * Resolve the client IP from the request. Only trusts the `X-Forwarded-For`
 * header when `ZENCODE_TRUST_PROXY` is enabled (for reverse-proxy setups).
 * Otherwise falls back to the direct socket address reported by the runtime,
 * and finally to `"unknown"`.
 */
export function getClientIp(c: Context): string {
  if (config.trustProxy) {
    const forwarded = c.req.header("x-forwarded-for");
    if (forwarded) {
      // Take the first address in the chain (original client)
      return forwarded.split(",")[0].trim();
    }
  }
  // Hono on Bun exposes the remote address via env
  const connInfo = c.env?.remoteAddress ?? c.env?.ip;
  if (connInfo) return String(connInfo);
  return "unknown";
}

/**
 * Create a rate-limiter instance from environment variables (with defaults).
 */
export function createRateLimiterFromEnv(): RateLimiter {
  const maxRequests = config.rateLimit;
  const windowMs = config.rateWindowMs;
  return new RateLimiter(maxRequests, windowMs);
}

/**
 * Hono middleware that enforces per-IP rate limiting.
 * Returns 429 with a `Retry-After` header when the limit is exceeded.
 */
export function rateLimitMiddleware(limiter: RateLimiter) {
  return async (c: Context, next: Next) => {
    const ip = getClientIp(c);
    const result = limiter.check(ip);

    // Always set informational rate-limit headers
    c.header("X-RateLimit-Limit", String(result.limit));
    c.header("X-RateLimit-Remaining", String(result.remaining));
    c.header("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));

    if (!result.allowed) {
      const retryAfterSeconds = Math.ceil((result.resetAt - Date.now()) / 1000);
      c.header("Retry-After", String(Math.max(1, retryAfterSeconds)));
      return c.json(
        { error: "Too many requests. Please try again later." },
        429,
      );
    }

    return next();
  };
}
