import { createHash, randomBytes, scrypt as nodeScrypt, timingSafeEqual } from "node:crypto";
import { and, eq, gt } from "drizzle-orm";
import { cookies } from "next/headers";
import { db } from "@/db";
import { authAccounts, authSessions, authUsers } from "@/db/schema";
import { currentUser } from "@/lib/auth";

function deriveKey(password: string, salt: Buffer, keyLength: number, options: { N: number; r: number; p: number; maxmem: number }) {
  return new Promise<Buffer>((resolve, reject) => nodeScrypt(password, salt, keyLength, options, (error, derived) => error ? reject(error) : resolve(derived as Buffer)));
}
const ADMIN_SESSION_TTL_SECONDS = 8 * 60 * 60;
export const ADMIN_SESSION_COOKIE = "kiln-admin-session";
export const ADMIN_CSRF_COOKIE = "kiln-admin-csrf";

type LoginAttempt = { count: number; firstAt: number; lockedUntil: number };
const loginAttempts = new Map<string, LoginAttempt>();

function clientKey(request: Request, login: string) {
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
  return `${ip}:${login.trim().toLowerCase()}`;
}

function isRateLimited(key: string) {
  const current = loginAttempts.get(key);
  if (!current) return false;
  const now = Date.now();
  if (current.lockedUntil > now) return true;
  if (now - current.firstAt > 15 * 60 * 1000) loginAttempts.delete(key);
  return false;
}

function recordFailedLogin(key: string) {
  const now = Date.now();
  const current = loginAttempts.get(key);
  const next = !current || now - current.firstAt > 15 * 60 * 1000
    ? { count: 1, firstAt: now, lockedUntil: 0 }
    : { ...current, count: current.count + 1 };
  if (next.count >= 5) next.lockedUntil = now + 15 * 60 * 1000;
  loginAttempts.set(key, next);
}

async function verifyPassword(password: string, encoded: string) {
  const [algorithm, cost, blockSize, parallelization, salt, digest] = encoded.split("$");
  if (algorithm !== "scrypt" || !cost || !blockSize || !parallelization || !salt || !digest) return false;
  try {
    const expected = Buffer.from(digest, "base64url");
    const actual = await deriveKey(password, Buffer.from(salt, "base64url"), expected.length, { N: Number(cost), r: Number(blockSize), p: Number(parallelization), maxmem: 64 * 1024 * 1024 });
    return actual.length === expected.length && timingSafeEqual(actual, expected);
  } catch {
    return false;
  }
}

export async function verifyAdminCredentials(request: Request, login: string, password: string) {
  const key = clientKey(request, login);
  if (isRateLimited(key)) return false;
  const expectedLogin = process.env.ADMIN_LOGIN;
  const expectedHash = process.env.ADMIN_PASSWORD_HASH;
  const validLogin = Boolean(expectedLogin && login === expectedLogin);
  const validPassword = expectedHash ? await verifyPassword(password, expectedHash) : false;
  if (!validLogin || !validPassword) {
    recordFailedLogin(key);
    return false;
  }
  loginAttempts.delete(key);
  return true;
}

async function getOrCreateAdminUser() {
  if (!db) throw new Error("Database is not configured");
  const login = process.env.ADMIN_LOGIN;
  if (!login) throw new Error("ADMIN_LOGIN is not configured");
  const [account] = await db.select().from(authAccounts).where(and(eq(authAccounts.provider, "admin"), eq(authAccounts.providerAccountId, login))).limit(1);
  if (account) return account.userId;
  const [user] = await db.insert(authUsers).values({ displayName: login, email: null, avatarUrl: null }).returning({ id: authUsers.id });
  await db.insert(authAccounts).values({ userId: user.id, provider: "admin", providerAccountId: login });
  return user.id;
}

export async function createAdminSession() {
  if (!db) throw new Error("Database is not configured");
  const userId = await getOrCreateAdminUser();
  const token = randomBytes(32).toString("base64url");
  await db.insert(authSessions).values({ tokenHash: createHash("sha256").update(token).digest("hex"), userId, expiresAt: new Date(Date.now() + ADMIN_SESSION_TTL_SECONDS * 1000) });
  return token;
}

async function currentLegacyAdminSession() {
  if (!db) return null;
  const token = (await cookies()).get(ADMIN_SESSION_COOKIE)?.value;
  if (!token) return null;
  const [row] = await db.select({ session: authSessions, user: authUsers }).from(authSessions).innerJoin(authUsers, eq(authUsers.id, authSessions.userId)).where(and(eq(authSessions.tokenHash, createHash("sha256").update(token).digest("hex")), gt(authSessions.expiresAt, new Date()))).limit(1);
  return row?.user || null;
}

export async function currentAdminSession() {
  const legacyAdmin = await currentLegacyAdminSession();
  if (legacyAdmin) return legacyAdmin;
  const user = await currentUser();
  return user?.role === "admin" ? user : null;
}

function isSameOrigin(request: Request) {
  if (request.headers.get("sec-fetch-site") === "cross-site") return false;
  const origin = request.headers.get("origin");
  if (!origin) return request.headers.get("sec-fetch-site") === "same-origin";
  try {
    const expectedHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim() || request.headers.get("host") || new URL(request.url).host;
    return new URL(origin).host === expectedHost;
  } catch { return false; }
}

export async function adminAuthorized(request: Request, mutating = false) {
  const legacyAdmin = await currentLegacyAdminSession();
  if (legacyAdmin && !mutating) return true;
  if (legacyAdmin) {
    const csrfCookie = (await cookies()).get(ADMIN_CSRF_COOKIE)?.value;
    const csrfHeader = request.headers.get("x-csrf-token");
    if (csrfCookie && csrfHeader && csrfCookie === csrfHeader) return true;
  }
  const user = await currentUser();
  if (user?.role !== "admin") return false;
  if (!mutating) return true;
  return isSameOrigin(request);
}

export function newCsrfToken() { return randomBytes(24).toString("base64url"); }

export function adminCookieOptions(maxAge: number) {
  return { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict" as const, maxAge, path: "/" };
}

export function csrfCookieOptions(maxAge: number) {
  return { httpOnly: false, secure: process.env.NODE_ENV === "production", sameSite: "strict" as const, maxAge, path: "/" };
}
