import "server-only";

import { createHash, randomBytes } from "node:crypto";
import { cookies, headers } from "next/headers";
import { cache } from "react";

import {
  SESSION_COOKIE,
  SESSION_IDLE_TIMEOUT_MS,
  SESSION_MAX_AGE_MS,
} from "@/lib/auth/constants";
import { db } from "@/lib/db";
import { clientIp } from "@/lib/request";

/** Only the hash is stored, so the database never holds a usable cookie value. */
const hashToken = (token: string) =>
  createHash("sha256").update(token).digest("hex");

/** Fields of the signed-in admin that are safe to pass to Client Components. */
export const adminUserSelect = {
  id: true,
  email: true,
  name: true,
  role: true,
  avatarUrl: true,
  isActive: true,
} as const;

export type CurrentAdmin = {
  id: string;
  email: string;
  name: string;
  role: "ADMIN" | "EDITOR";
  avatarUrl: string | null;
};

export async function createSession(userId: string) {
  const token = randomBytes(32).toString("base64url");
  const now = Date.now();
  const requestHeaders = await headers();

  await db.adminSession.create({
    data: {
      id: hashToken(token),
      userId,
      expiresAt: new Date(now + SESSION_IDLE_TIMEOUT_MS),
      ipAddress: clientIp(requestHeaders).slice(0, 64),
      userAgent: requestHeaders.get("user-agent")?.slice(0, 255) ?? null,
    },
  });

  const cookieStore = await cookies();
  cookieStore.set(SESSION_COOKIE, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    expires: new Date(now + SESSION_MAX_AGE_MS),
  });
}

/**
 * The session for this request, or null. Deduplicated per request, so
 * layouts, pages and actions can all call it freely.
 */
export const getSession = cache(async () => {
  const cookieStore = await cookies();
  const token = cookieStore.get(SESSION_COOKIE)?.value;
  if (!token) return null;

  const session = await db.adminSession.findUnique({
    where: { id: hashToken(token) },
    include: { user: { select: adminUserSelect } },
  });

  const now = Date.now();
  if (
    !session ||
    !session.user.isActive ||
    session.expiresAt.getTime() <= now ||
    session.createdAt.getTime() + SESSION_MAX_AGE_MS <= now
  ) {
    return null;
  }

  // Slide the idle deadline forward, at most once an hour to spare the database.
  if (now - session.lastUsedAt.getTime() > 60 * 60 * 1000) {
    await db.adminSession.update({
      where: { id: session.id },
      data: {
        lastUsedAt: new Date(now),
        expiresAt: new Date(
          Math.min(
            now + SESSION_IDLE_TIMEOUT_MS,
            session.createdAt.getTime() + SESSION_MAX_AGE_MS,
          ),
        ),
      },
    });
  }

  const { user } = session;
  return {
    id: session.id,
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      avatarUrl: user.avatarUrl,
    } satisfies CurrentAdmin,
  };
});

/** Ends the current session in the database and clears the cookie. */
export async function destroySession() {
  const cookieStore = await cookies();
  const token = cookieStore.get(SESSION_COOKIE)?.value;
  if (token) {
    await db.adminSession.deleteMany({ where: { id: hashToken(token) } });
  }
  cookieStore.delete(SESSION_COOKIE);
}

/** Signs a user out everywhere except, optionally, the current session. */
export async function destroyOtherSessions(userId: string) {
  const current = await getSession();
  await db.adminSession.deleteMany({
    where: { userId, ...(current && { id: { not: current.id } }) },
  });
}

export async function purgeExpiredSessions() {
  await db.adminSession.deleteMany({
    where: { expiresAt: { lt: new Date() } },
  });
}
