import { randomBytes, timingSafeEqual } from 'node:crypto';
import argon2 from 'argon2';
import session from 'express-session';
import { PrismaSessionStore } from './session-store.js';
import { Router, type RequestHandler } from 'express';
import { rateLimit } from 'express-rate-limit';
import type { PrismaClient } from '../generated/prisma/client.js';
import { loginSchema, profileSchema, type AdminDto } from '../shared/contracts.js';
import { HttpError } from './errors.js';

declare module 'express-session' {
  interface SessionData { csrfToken: string; adminId: number; authVersion: number }
}
declare global {
  namespace Express { interface Request { admin?: AdminDto } }
}
export const passwordOptions = { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 } as const;
export function authSetup(db: PrismaClient, config: { databaseUrl: string; sessionSecret: string; origin: string; production: boolean; loginLimit?: number }) {
  const store = new PrismaSessionStore(db);
  const middleware = session({
    store, name: 'unair.sid', secret: config.sessionSecret, resave: false, saveUninitialized: false,
    cookie: { httpOnly: true, sameSite: 'lax', secure: config.production, maxAge: 12 * 60 * 60 * 1000 },
  });
  const csrf: RequestHandler = (req, _res, next) => {
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
    const origin = req.get('origin');
    if (!origin || origin !== config.origin) return next(new HttpError(403, 'Asal permintaan tidak diizinkan.'));
    const token = req.get('x-csrf-token') ?? '';
    const expected = req.session.csrfToken ?? '';
    if (!expected || !/^[a-f0-9]{64}$/.test(token) || !timingSafeEqual(Buffer.from(token), Buffer.from(expected))) return next(new HttpError(403, 'Sesi formulir berakhir. Muat ulang halaman.'));
    next();
  };
  const requireAdmin: RequestHandler = async (req, _res, next) => {
    try {
      const user = req.session.adminId ? await db.adminUser.findUnique({ where: { id: req.session.adminId } }) : null;
      if (!user?.active || user.authVersion !== req.session.authVersion) {
        if (req.session.adminId) await new Promise<void>((resolve, reject) => req.session.destroy(err => err ? reject(err) : resolve()));
        throw new HttpError(401, 'Silakan masuk sebagai admin.');
      }
      req.admin = { id: user.id, name: user.name, email: user.email };
      next();
    } catch (error) { next(error); }
  };
  const router = Router();
  router.get('/session', async (req, res) => {
    const user = req.session.adminId ? await db.adminUser.findUnique({ where: { id: req.session.adminId } }) : null;
    const valid = user?.active && user.authVersion === req.session.authVersion;
    if (req.session.adminId && !valid) {
      await new Promise<void>((resolve, reject) => req.session.regenerate(err => err ? reject(err) : resolve()));
    }
    req.session.csrfToken ??= randomBytes(32).toString('hex');
    res.json({ user: valid ? { id: user.id, name: user.name, email: user.email } : null, csrfToken: req.session.csrfToken });
  });
  const limitOptions = { windowMs: 15 * 60 * 1000, standardHeaders: 'draft-8' as const, legacyHeaders: false, message: { error: { message: 'Terlalu banyak percobaan masuk. Coba lagi dalam 15 menit.' } } };
  const ipLimit = rateLimit({ ...limitOptions, limit: config.loginLimit ?? 10 });
  const emailLimit = rateLimit({ ...limitOptions, limit: config.loginLimit ?? 5, keyGenerator: req => String(req.body?.email ?? '').trim().toLowerCase() });
  // A real hash keeps the password verification work similar for unknown emails.
  const dummyHash = argon2.hash(randomBytes(32), passwordOptions);
  router.post('/login', ipLimit, emailLimit, async (req, res) => {
    const data = loginSchema.parse(req.body);
    const user = await db.adminUser.findUnique({ where: { email: data.email } });
    const valid = await argon2.verify(user?.passwordHash ?? await dummyHash, data.password);
    if (!user?.active || !valid) throw new HttpError(401, 'Email atau kata sandi tidak sesuai.');
    await new Promise<void>((resolve, reject) => req.session.regenerate(err => err ? reject(err) : resolve()));
    req.session.adminId = user.id;
    req.session.authVersion = user.authVersion;
    req.session.csrfToken = randomBytes(32).toString('hex');
    res.json({ user: { id: user.id, name: user.name, email: user.email }, csrfToken: req.session.csrfToken });
  });
  router.post('/logout', async (req, res) => {
    await new Promise<void>((resolve, reject) => req.session.destroy(err => err ? reject(err) : resolve()));
    res.clearCookie('unair.sid', { httpOnly: true, sameSite: 'lax', secure: config.production });
    res.status(204).end();
  });
  router.patch('/profile', requireAdmin, async (req, res) => {
    const data = profileSchema.parse(req.body);
    const user = await db.adminUser.findUniqueOrThrow({ where: { id: req.admin!.id } });
    if (data.newPassword && !await argon2.verify(user.passwordHash, data.currentPassword ?? '')) throw new HttpError(422, 'Kata sandi saat ini tidak sesuai.');
    await db.adminUser.update({ where: { id: user.id }, data: { name: data.name, ...(data.newPassword ? { passwordHash: await argon2.hash(data.newPassword, passwordOptions), authVersion: { increment: 1 } } : {}) } });
    if (data.newPassword) {
      await new Promise<void>((resolve, reject) => req.session.destroy(err => err ? reject(err) : resolve()));
      res.clearCookie('unair.sid');
    }
    res.json({ passwordChanged: !!data.newPassword });
  });
  return { middleware, csrf, requireAdmin, router, close: () => store.close() };
}
