/**
 * Rate limiting REST endpoints.
 *
 * POST   /ratelimit/check     — check (and record) a request against limits
 * POST   /ratelimit/reset     — reset rate limit for a key
 * GET    /ratelimit/config    — get current rate limit config
 * PUT    /ratelimit/config    — update rate limit config (admin)
 */

import type { FastifyInstance } from 'fastify';
import { DEFAULT_PRODUCT_ID } from '../../lib/product-config.js';
import { getRequestProductId } from '../../lib/request-context.js';
import { BadRequestError } from '../../lib/errors.js';
import { CheckRateLimitSchema, RateLimitConfigSchema } from './types.js';
import type { RateLimitRule, RateLimitConfig } from './types.js';
import * as store from './store.js';

/**
 * Default rate limit rules — configurable per product via
 * RATE_LIMIT_CONFIG_JSON env var.
 */
function loadConfig(): Map<string, RateLimitConfig> {
  const configs = new Map<string, RateLimitConfig>();

  const json = process.env.RATE_LIMIT_CONFIG_JSON;
  if (json) {
    try {
      const parsed = JSON.parse(json) as RateLimitConfig[];
      for (const c of parsed) configs.set(c.productId, c);
      return configs;
    } catch {
      /* fall through to defaults */
    }
  }

  // Sensible defaults
  configs.set(DEFAULT_PRODUCT_ID, {
    productId: DEFAULT_PRODUCT_ID,
    rules: [
      { maxRequests: 60, windowSeconds: 60 }, // 60 req/min global
      { maxRequests: 5, windowSeconds: 60, routePrefix: '/api/auth' }, // 5 auth attempts/min
      { maxRequests: 10, windowSeconds: 60, routePrefix: '/api/stripe' }, // 10 stripe calls/min
      // Diagnostics rate limits (Phase 1.5)
      { maxRequests: 10, windowSeconds: 3600, routePrefix: '/api/diagnostics:session:create' }, // 10 session creates/hour per admin
      { maxRequests: 12, windowSeconds: 60, routePrefix: '/api/diagnostics:config:poll' }, // 1 per 5 sec per device (12/min)
      { maxRequests: 100, windowSeconds: 60, routePrefix: '/api/diagnostics:ingest:submit' }, // 100 ingests/min per device
    ],
  });

  return configs;
}

const configMap = loadConfig();

function findRule(productId: string, routePrefix?: string): RateLimitRule {
  const config = configMap.get(productId);
  if (!config) {
    return { maxRequests: 60, windowSeconds: 60 }; // fallback
  }

  // Find most specific matching rule
  if (routePrefix) {
    const specific = config.rules.find(r => r.routePrefix && routePrefix.startsWith(r.routePrefix));
    if (specific) return specific;
  }

  // Fall back to global rule (no routePrefix)
  const global = config.rules.find(r => !r.routePrefix);
  return global ?? { maxRequests: 60, windowSeconds: 60 };
}

export async function rateLimitRoutes(app: FastifyInstance) {
  // Check + record
  app.post('/ratelimit/check', async (req, reply) => {
    const parsed = CheckRateLimitSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const { key, productId, routePrefix } = parsed.data;
    const rule = findRule(productId, routePrefix);
    const compositeKey = `${productId}:${key}${routePrefix ? `:${routePrefix}` : ''}`;
    const result = await store.checkAndRecord(compositeKey, rule);

    if (!result.allowed) {
      reply.code(429).header('Retry-After', String(Math.ceil((result.retryAfterMs ?? 0) / 1000)));
    }
    return result;
  });

  // Reset
  app.post('/ratelimit/reset', async req => {
    const { key, productId } = req.body as { key?: string; productId?: string };
    const pid = productId || getRequestProductId(req);
    if (!key) throw new BadRequestError('key is required');
    await store.reset(`${pid}:${key}`);
    return { reset: true };
  });

  // Get config
  app.get('/ratelimit/config', async req => {
    const { productId } = req.query as { productId?: string };
    const pid = productId || getRequestProductId(req);
    const config = configMap.get(pid);
    return config ?? { productId: pid, rules: [] };
  });

  // Update config (admin)
  app.put('/ratelimit/config', async req => {
    const parsed = RateLimitConfigSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    configMap.set(parsed.data.productId, parsed.data);
    return parsed.data;
  });
}
