import { NextResponse } from 'next/server';
import crypto from 'crypto';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { initDatabase } from '@server/db/init';
import { checkRateLimit } from './withRateLimit';
import { v1ErrorBody } from './v1Errors';
import type { RouteContext, RouteHandler } from './withErrorHandler';
import { getPlanLimits } from '@server/utils/features';

import { childLogger } from '@server/logger';
const log = childLogger('middleware.api-key');

/**
 * Authenticated API key context attached to the request once a Bearer
 * `rsk_<hex>` token has been validated. Routes read `req.apiKey.restaurantId`
 * for tenant scoping just like dashboard handlers read `req.session.restaurantId`.
 */
export type ApiKeyContext = {
  id: string;
  restaurantId: string;
  name: string;
  permissions: string[];
  keyHash: string;
};

export type ApiKeyRequest = Request & { apiKey: ApiKeyContext };

export type ApiKeyHandler = (
  req: ApiKeyRequest,
  context: RouteContext
) => Promise<NextResponse | Response>;

const PER_MIN_LIMIT = 60;
const PER_DAY_LIMIT = 5000;
const PER_MIN_WINDOW_MS = 60_000;
const PER_DAY_WINDOW_MS = 24 * 60 * 60 * 1000;

function unauthorized(message = 'Invalid or missing API key'): NextResponse {
  return NextResponse.json(
    v1ErrorBody(message, 'AUTH_ERROR'),
    {
      status: 401,
      headers: { 'WWW-Authenticate': 'Bearer realm="restroagent-api"' },
    }
  );
}

function forbidden(message: string): NextResponse {
  return NextResponse.json(v1ErrorBody(message, 'FORBIDDEN'), { status: 403 });
}

function rateLimited(retryAfterSec: number, scope: string): NextResponse {
  return NextResponse.json(
    v1ErrorBody(
      'Too many requests. Slow down and retry after the cooldown.',
      'RATE_LIMITED',
      { scope, retry_after_seconds: retryAfterSec }
    ),
    {
      status: 429,
      headers: { 'Retry-After': String(retryAfterSec) },
    }
  );
}

function extractBearer(req: Request): string | null {
  const header = req.headers.get('authorization') || req.headers.get('Authorization');
  if (!header) return null;
  const match = header.match(/^Bearer\s+(\S+)$/i);
  if (!match) return null;
  const token = match[1].trim();
  // Reject tokens that don't carry our prefix early so we never hash and
  // probe the database with arbitrary client-supplied bytes.
  if (!token.startsWith('rsk_')) return null;
  return token;
}

function hashKey(rawKey: string): string {
  return crypto.createHash('sha256').update(rawKey).digest('hex');
}

/**
 * Fire-and-forget update of usage counters. We don't await this in the hot
 * path because the API call itself doesn't depend on the counter being
 * persisted; instead we log on failure.
 *
 * `current_period_start` is reset to the start of the current month any time
 * we increment after the stored timestamp falls into a previous month. This
 * gives us a self-healing monthly reset without needing a cron job.
 */
function incrementUsage(keyId: string): void {
  Promise.resolve()
    .then(() =>
      db.execute(sql`
        UPDATE api_keys SET
          calls_this_month = CASE
            WHEN current_period_start IS NULL
              OR current_period_start < date_trunc('month', NOW())
              THEN 1
            ELSE calls_this_month + 1
          END,
          current_period_start = CASE
            WHEN current_period_start IS NULL
              OR current_period_start < date_trunc('month', NOW())
              THEN date_trunc('month', NOW())
            ELSE current_period_start
          END,
          last_used_at = NOW(),
          updated_at = NOW()
        WHERE id = ${keyId}
      `)
    )
    .catch((err) => {
      log.warn({ err }, 'withApiKey: usage counter update failed');
    });
}

/**
 * Bearer-token authentication middleware for `/api/v1/**` routes.
 *
 * Order of operations:
 *   1. Extract & format-check the Bearer token (must start with `rsk_`).
 *   2. Sha256-hash it and look up an active row in `api_keys`.
 *   3. Apply two per-key rate-limit buckets (60/min and 5000/day) using the
 *      shared in-process limiter.
 *   4. Optionally enforce a permission string (e.g. `orders:read`).
 *   5. Attach the resolved key context to the request and invoke the handler.
 *   6. After the response is produced, fire-and-forget bump the usage counters.
 *
 * Note on multi-instance deployments: the in-process rate limiter is per-pod.
 * That's acceptable here because (a) each public key is scoped to a single
 * customer, and (b) the daily cap is enforced loosely — a customer running on
 * N pods can burst up to N × 5000/day, which is still small enough to be
 * a non-issue in this app's deployment topology.
 */
export function withApiKey(
  handler: ApiKeyHandler,
  opts: { permission?: string } = {}
): RouteHandler {
  return async (req: Request, context: RouteContext) => {
    const rawKey = extractBearer(req);
    if (!rawKey) return unauthorized('Missing or malformed API key. Use `Authorization: Bearer rsk_…`.');

    await initDatabase();
    const keyHash = hashKey(rawKey);

    /* raw: SELECT id, restaurant_id, name, permissions, status FROM api_keys WHERE key_hash = $1 LIMIT 1 */
    const { rows } = await db.execute(sql`
      SELECT id, restaurant_id, name, permissions, status
        FROM api_keys
       WHERE key_hash = ${keyHash}
       LIMIT 1
    `);
    const row = rows[0] as
      | {
          id: string;
          restaurant_id: string;
          name: string;
          permissions: unknown;
          status: string;
        }
      | undefined;

    if (!row) return unauthorized();
    if (row.status !== 'active') return unauthorized('API key has been revoked or disabled.');

    const permissions = Array.isArray(row.permissions) ? (row.permissions as string[]) : [];

    if (opts.permission && !permissions.includes(opts.permission)) {
      return forbidden(`This API key is missing the required permission: ${opts.permission}`);
    }

    // Per-key throttle. We bucket on the key id so a leaked key can be
    // revoked without affecting the rest of the restaurant's traffic.
    // The per-minute cap comes from the restaurant's plan when set; fall
    // back to the hardcoded default so existing behaviour is unchanged.
    let planMinLimit = PER_MIN_LIMIT;
    try {
      const limits = await getPlanLimits(row.restaurant_id);
      if (limits.api_calls_per_minute !== null) {
        planMinLimit = limits.api_calls_per_minute;
      }
    } catch {
      // Fall back to default on any DB error
    }

    const minResult = checkRateLimit('apiv1:min', row.id, planMinLimit, PER_MIN_WINDOW_MS);
    if (!minResult.allowed) return rateLimited(minResult.retryAfterSec ?? 60, 'minute');
    const dayResult = checkRateLimit('apiv1:day', row.id, PER_DAY_LIMIT, PER_DAY_WINDOW_MS);
    if (!dayResult.allowed) return rateLimited(dayResult.retryAfterSec ?? 3600, 'day');

    const apiKey: ApiKeyContext = {
      id: row.id,
      restaurantId: row.restaurant_id,
      name: row.name,
      permissions,
      keyHash,
    };

    Object.defineProperty(req, 'apiKey', {
      value: apiKey,
      writable: false,
      enumerable: true,
      configurable: true,
    });

    const response = await handler(req as ApiKeyRequest, context);
    incrementUsage(row.id);
    return response;
  };
}
