import { Context } from 'hono';
import { getRateLimiter } from './stores/rateLimiter.js';

/**
 * Rate limiting helpers.
 *
 * The actual counting is delegated to a pluggable rate limiter (in-memory by
 * default, Redis-backed when REDIS_URL is set) so limits are enforced either
 * per instance (single-instance deployments) or across all instances
 * (horizontal scaling). See lib/stores/rateLimiter.ts.
 */

// Peers allowed to speak for the client via X-Real-IP / X-Forwarded-For:
// loopback (nginx on the same host, the Next.js proxy) and RFC1918 ranges
// (the docker-compose scale deployment, where nginx is another container).
// Extend with TRUSTED_PROXIES (comma-separated IP prefixes, e.g. "172.20.").
const DEFAULT_TRUSTED_PROXY_PREFIXES = ['127.', '10.', '192.168.', '::1'];

function trustedProxyPrefixes(): string[] {
  const extra = (process.env.TRUSTED_PROXIES || '')
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean);
  return [...DEFAULT_TRUSTED_PROXY_PREFIXES, ...extra];
}

/** Strip the IPv4-mapped IPv6 prefix so "::ffff:127.0.0.1" matches "127.". */
function normalizeIp(ip: string | undefined | null): string {
  const trimmed = (ip || '').trim();
  return trimmed.toLowerCase().startsWith('::ffff:') ? trimmed.slice(7) : trimmed;
}

export function isTrustedProxyIp(ip: string): boolean {
  const normalized = normalizeIp(ip);
  if (!normalized) return false;
  if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(normalized)) return true; // 172.16.0.0/12
  return trustedProxyPrefixes().some((prefix) => normalized === prefix || normalized.startsWith(prefix));
}

// Rough shape check so a junk header value can't become a rate-limit key.
function looksLikeIp(value: string): boolean {
  return value.length > 0 && value.length <= 45 && /^[0-9a-fA-F.:]+$/.test(value);
}

/**
 * Spoof-resistant client IP resolution.
 *
 * The TCP peer address (via @hono/node-server's env.incoming) anchors the
 * trust decision: forwarded headers are only honoured when the direct peer is
 * one of our own proxies. X-Real-IP is preferred because nginx overwrites it
 * at the edge (deploy/*.conf); X-Forwarded-For is append-only, so it is
 * walked from the right past our proxy hops — the leftmost entries are
 * client-controlled and never trusted on their own.
 */
export function getClientIp(c: Context): string {
  const socketAddr = normalizeIp((c.env as any)?.incoming?.socket?.remoteAddress);

  if (socketAddr && isTrustedProxyIp(socketAddr)) {
    const realIp = normalizeIp(c.req.header('x-real-ip'));
    if (realIp && looksLikeIp(realIp)) return realIp;

    const forwarded = c.req.header('x-forwarded-for');
    if (forwarded) {
      const chain = forwarded.split(',').map((s) => normalizeIp(s)).filter(Boolean);
      for (let i = chain.length - 1; i >= 0; i--) {
        if (!isTrustedProxyIp(chain[i])) {
          return looksLikeIp(chain[i]) ? chain[i] : socketAddr;
        }
      }
      // Every hop is one of ours: a genuinely local/internal client.
    }
  }

  return socketAddr || 'unknown';
}

/**
 * Consume one unit against a key. Returns whether the request is allowed and,
 * when blocked, how many seconds until the window resets.
 */
export function consumeRateLimit(
  key: string,
  max: number,
  windowMs: number
): Promise<{ allowed: boolean; retryAfter?: number }> {
  return getRateLimiter().consume(key, max, windowMs);
}

/**
 * Hono middleware factory that rate-limits by client IP.
 * Use a distinct `prefix` per endpoint group so unrelated routes don't share a bucket.
 */
export function rateLimitMiddleware(opts: { max: number; windowMs: number; prefix: string }) {
  return async (c: Context, next: () => Promise<void>) => {
    const ip = getClientIp(c);
    const result = await consumeRateLimit(`${opts.prefix}:${ip}`, opts.max, opts.windowMs);
    if (!result.allowed) {
      return c.json(
        { error: 'Too many requests. Please try again later.', retryAfter: result.retryAfter },
        429
      );
    }
    await next();
  };
}
