import { NextResponse } from "next/server";
import { query } from "./db";
import { getClientIp, getUserAgent } from "./request-meta";
import { safeError } from "./safe-log";

export type SecuritySeverity = "low" | "medium" | "high" | "critical";

type RateBucket = { count: number; resetAt: number };

/** In-memory rate limit counters (per process). Key = `${group}:${ip}` */
const rateBuckets = new Map<string, RateBucket>();

const DEFAULT_WINDOW_MS = 10 * 60 * 1000;
const DEFAULT_MAX = 30;

export async function recordSecurityEvent(input: {
  category: string;
  severity: SecuritySeverity;
  title: string;
  detail?: Record<string, unknown> | null;
  sourceIp?: string | null;
}) {
  try {
    await query(
      `INSERT INTO security_events (category, severity, title, detail, source_ip)
       VALUES (:category, :severity, :title, :detail, :source_ip)`,
      {
        category: input.category,
        severity: input.severity,
        title: input.title.slice(0, 160),
        detail: JSON.stringify(input.detail || {}),
        source_ip: input.sourceIp?.slice(0, 64) || null,
      },
    );
  } catch (e) {
    safeError("recordSecurityEvent failed", e);
  }
}

export async function isIpBlocked(ip: string | null | undefined): Promise<boolean> {
  if (!ip) return false;
  try {
    const rows = await query<{ id: number }[]>(
      `SELECT id FROM network_blocklist
       WHERE client_ip = :ip
         AND (expires_at IS NULL OR expires_at > NOW())
       LIMIT 1`,
      { ip: ip.slice(0, 64) },
    );
    const list = rows as unknown as { id: number }[];
    return Boolean(list?.[0]?.id);
  } catch (e) {
    safeError("isIpBlocked failed", e);
    return false;
  }
}

export async function recordHttpEvent(input: {
  app?: "website" | "admin";
  method: string;
  pathNorm: string;
  statusCode: number;
  latencyMs: number;
  clientIp: string;
  userAgent?: string | null;
  bytesIn?: number | null;
  bytesOut?: number | null;
  rateLimitAction?: "allowed" | "throttled" | "blocked";
}) {
  try {
    await query(
      `INSERT INTO http_request_events
        (app, method, path_norm, status_code, latency_ms, client_ip, user_agent,
         bytes_in, bytes_out, rate_limit_action)
       VALUES
        (:app, :method, :path_norm, :status_code, :latency_ms, :client_ip, :user_agent,
         :bytes_in, :bytes_out, :rate_limit_action)`,
      {
        app: input.app || "website",
        method: input.method.slice(0, 10),
        path_norm: input.pathNorm.slice(0, 200),
        status_code: input.statusCode,
        latency_ms: Math.max(0, Math.round(input.latencyMs)),
        client_ip: input.clientIp.slice(0, 64),
        user_agent: input.userAgent?.slice(0, 255) || null,
        bytes_in: input.bytesIn ?? null,
        bytes_out: input.bytesOut ?? null,
        rate_limit_action: input.rateLimitAction || "allowed",
      },
    );
  } catch {
    // best-effort — never fail the request
  }
}

export type RateLimitResult = {
  allowed: boolean;
  count: number;
  max: number;
  retryAfterSec: number;
};

/**
 * In-memory sliding-window style counter (fixed window per key).
 * Also writes a security_event when the limit is exceeded.
 */
export function checkRateLimit(
  ip: string,
  routeGroup: string,
  opts?: { windowMs?: number; max?: number },
): RateLimitResult {
  const windowMs = opts?.windowMs ?? DEFAULT_WINDOW_MS;
  const max = opts?.max ?? DEFAULT_MAX;
  const key = `${routeGroup}:${ip}`;
  const now = Date.now();
  let bucket = rateBuckets.get(key);
  if (!bucket || bucket.resetAt <= now) {
    bucket = { count: 0, resetAt: now + windowMs };
    rateBuckets.set(key, bucket);
  }
  bucket.count += 1;
  const allowed = bucket.count <= max;
  const retryAfterSec = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));

  if (!allowed && bucket.count === max + 1) {
    void recordSecurityEvent({
      category: "network",
      severity: "high",
      title: `Rate limit exceeded: ${routeGroup}`,
      detail: { routeGroup, count: bucket.count, max, windowMs },
      sourceIp: ip,
    });
  }

  return { allowed, count: bucket.count, max, retryAfterSec };
}

/**
 * Guard for registration API routes: blocklist → rate limit → then handler.
 * Records http_request_events asynchronously (best-effort).
 */
export async function withRegistrationSecurity(
  req: Request,
  opts: {
    routeGroup: string;
    pathNorm: string;
    windowMs?: number;
    max?: number;
  },
  handler: () => Promise<NextResponse>,
): Promise<NextResponse> {
  const started = Date.now();
  const ip = getClientIp(req) || "unknown";
  const ua = getUserAgent(req);
  const method = req.method || "POST";

  const finish = (
    res: NextResponse,
    rateLimitAction: "allowed" | "throttled" | "blocked",
  ) => {
    const latencyMs = Date.now() - started;
    void recordHttpEvent({
      method,
      pathNorm: opts.pathNorm,
      statusCode: res.status,
      latencyMs,
      clientIp: ip,
      userAgent: ua,
      rateLimitAction,
    });
    return res;
  };

  if (await isIpBlocked(ip === "unknown" ? null : ip)) {
    void recordSecurityEvent({
      category: "network",
      severity: "medium",
      title: "Blocked IP hit registration API",
      detail: { path: opts.pathNorm, routeGroup: opts.routeGroup },
      sourceIp: ip === "unknown" ? null : ip,
    });
    return finish(
      NextResponse.json(
        { ok: false, error: "Access denied.", code: "ip_blocked" },
        { status: 403 },
      ),
      "blocked",
    );
  }

  const rl = checkRateLimit(ip, opts.routeGroup, {
    windowMs: opts.windowMs,
    max: opts.max,
  });
  if (!rl.allowed) {
    return finish(
      NextResponse.json(
        {
          ok: false,
          error: "Too many requests. Please wait and try again.",
          code: "rate_limited",
          retry_after: rl.retryAfterSec,
        },
        {
          status: 429,
          headers: { "Retry-After": String(rl.retryAfterSec) },
        },
      ),
      "throttled",
    );
  }

  const res = await handler();
  return finish(res, "allowed");
}

/** Record a human_check rejection as a security event (in addition to audit_logs). */
export function recordHumanCheckReject(input: {
  at: string;
  reason: string;
  score: number;
  sourceIp?: string | null;
}) {
  void recordSecurityEvent({
    category: "network",
    severity: "medium",
    title: `Human check rejected: ${input.reason}`,
    detail: {
      at: input.at,
      reason: input.reason,
      score: input.score,
    },
    sourceIp: input.sourceIp,
  });
}
