import { createTRPCContext, loadRoleDefaults } from "@capakraken/api";
import { appRouter } from "@capakraken/api/router";
import { prisma } from "@capakraken/db";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { getToken } from "next-auth/jwt";
import type { NextRequest } from "next/server";
import { auth } from "~/server/auth.js";

function extractClientIp(req: NextRequest): string | null {
  const forwarded = req.headers.get("x-forwarded-for");
  if (forwarded) {
    const first = forwarded.split(",")[0]?.trim();
    if (first) return first;
  }
  const realIp = req.headers.get("x-real-ip");
  if (realIp) return realIp.trim();
  return null;
}

// Hard cap on tRPC request body size to prevent memory/CPU amplification from
// a single oversized payload. Stream uploads (files, reports) don't go through
// tRPC. 2 MiB is comfortably above any legitimate tRPC batch call.
const MAX_TRPC_BODY_BYTES = 2 * 1024 * 1024;

// Throttle lastActiveAt updates: max once per 60s per user
const lastActiveCache = new Map<string, number>();
const ACTIVITY_THROTTLE_MS = 60_000;

function trackActivity(userId: string) {
  const now = Date.now();
  const last = lastActiveCache.get(userId) ?? 0;
  if (now - last < ACTIVITY_THROTTLE_MS) return;
  lastActiveCache.set(userId, now);
  prisma.user
    .update({
      where: { id: userId },
      data: { lastActiveAt: new Date(now) },
    })
    .catch(() => {
      /* ignore */
    });
}

const handler = async (req: NextRequest) => {
  // Reject oversized bodies before we touch auth, DB, or the router. A tRPC
  // mutation should never exceed MAX_TRPC_BODY_BYTES. Content-Length is
  // advisory — also guard against chunked requests below via length check
  // on the cloned body.
  if (req.method !== "GET") {
    const declaredLength = req.headers.get("content-length");
    if (declaredLength) {
      const parsed = Number(declaredLength);
      if (Number.isFinite(parsed) && parsed > MAX_TRPC_BODY_BYTES) {
        return new Response(JSON.stringify({ error: "Request body too large" }), {
          status: 413,
          headers: { "Content-Type": "application/json" },
        });
      }
    }
  }

  const session = await auth();

  // Validate active session registry on every authenticated request.
  // Sessions kicked by concurrent-session limits or manual logout are rejected immediately.
  // Fail-open: if the table doesn't exist yet (pending migration) the check is skipped.
  // In E2E test mode the jwt callback skips registration, so skip validation too.
  //
  // We decode the JWT directly (not session.user.jti) because the session
  // token is client-visible and therefore must not carry internal
  // session-revocation identifiers — see security ticket #41.
  const isE2eTestMode = process.env["E2E_TEST_MODE"] === "true";
  if (session?.user && !isE2eTestMode) {
    const secret = process.env["AUTH_SECRET"] ?? process.env["NEXTAUTH_SECRET"] ?? "";
    const cookieName =
      (process.env["AUTH_URL"] ?? "").startsWith("https://") || process.env["VERCEL"] === "1"
        ? "__Host-authjs.session-token"
        : "authjs.session-token";
    const jwt = secret ? await getToken({ req, secret, salt: cookieName }) : null;
    const jti = (jwt?.["sid"] as string | undefined) ?? undefined;
    if (jti) {
      try {
        const activeSession = await prisma.activeSession.findUnique({ where: { jti } });
        if (!activeSession) {
          return new Response(JSON.stringify({ error: "Session revoked" }), {
            status: 401,
            headers: { "Content-Type": "application/json" },
          });
        }
      } catch {
        // Table may not exist yet (migration pending) — skip validation rather than crashing.
      }
    }
  }

  const dbUser = session?.user?.email
    ? await prisma.user.findUnique({
        where: { email: session.user.email },
        select: { id: true, systemRole: true, permissionOverrides: true },
      })
    : null;

  // Track user activity (throttled, fire-and-forget)
  if (dbUser) trackActivity(dbUser.id);

  // Load configurable role defaults (cached, 60s TTL)
  const roleDefaults = await loadRoleDefaults();

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const options: any = {
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () =>
      createTRPCContext({ session, dbUser, roleDefaults, clientIp: extractClientIp(req) }),
  };

  if (process.env["NODE_ENV"] === "development") {
    options.onError = ({
      path,
      error,
    }: {
      path?: string;
      error: { message: string; code?: string };
    }) => {
      const label = `tRPC ${path ?? "<no-path>"}`;
      if (error.code === "NOT_FOUND") {
        console.warn(`⚠️ ${label}: ${error.message}`);
        return;
      }
      console.error(`❌ ${label}: ${error.message}`);
    };
  }

  return fetchRequestHandler(options);
};

export { handler as GET, handler as POST };
