import { createHash, randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";
import type { NextFunction, Request, Response } from "express";
import { and, eq, gt, isNull } from "drizzle-orm";
import { db, yeSessionsTable, yeUsersTable } from "@workspace/db";

const scrypt = promisify(scryptCallback);
const SESSION_COOKIE = "ye_session";
const CSRF_COOKIE = "ye_csrf";
const SESSION_TTL_MS = 8 * 60 * 60 * 1000;
const IMPERSONATION_TTL_MS = 30 * 60 * 1000;
const MOBILE_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const IMPERSONATION_TOKEN_HEADER = "x-impersonation-token";

export const ROLES = ["Web Master", "super_admin", "admin", "hr", "secretary", "accounting", "supervisor", "employee"] as const;
export type Role = (typeof ROLES)[number];
export type AuthenticatedUser = typeof yeUsersTable.$inferSelect;

declare global {
  namespace Express {
    interface Request {
      auth?: { user: AuthenticatedUser; sessionId: string; sessionType: "web" | "mobile" | "impersonation"; csrfToken?: string };
    }
  }
}

const digest = (value: string) => createHash("sha256").update(value).digest("hex");

export async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(16).toString("hex");
  const derived = (await scrypt(password, salt, 64)) as Buffer;
  return `scrypt$${salt}$${derived.toString("hex")}`;
}

export async function verifyPassword(password: string, encoded: string | null): Promise<boolean> {
  if (!encoded?.startsWith("scrypt$")) return false;
  const [, salt, expectedHex] = encoded.split("$");
  if (!salt || !expectedHex) return false;
  const expected = Buffer.from(expectedHex, "hex");
  const actual = (await scrypt(password, salt, expected.length)) as Buffer;
  return expected.length === actual.length && timingSafeEqual(expected, actual);
}

export async function createSession(
  user: AuthenticatedUser,
  sessionType: "web" | "mobile" | "impersonation",
  req: Request,
  context?: { impersonatorUserId?: string; impersonatedUserId?: string },
) {
  const token = randomBytes(32).toString("base64url");
  const csrfToken = sessionType === "web" || sessionType === "impersonation"
    ? randomBytes(32).toString("base64url")
    : undefined;
  const expiresAt = new Date(Date.now() + (
    sessionType === "web"
      ? SESSION_TTL_MS
      : sessionType === "impersonation"
        ? IMPERSONATION_TTL_MS
        : MOBILE_SESSION_TTL_MS
  ));
  const [session] = await db.insert(yeSessionsTable).values({
    userId: user.id,
    tokenHash: digest(token),
    sessionType,
    impersonatorUserId: context?.impersonatorUserId,
    impersonatedUserId: context?.impersonatedUserId,
    csrfTokenHash: csrfToken ? digest(csrfToken) : null,
    expiresAt,
    ipAddress: req.ip,
    userAgent: req.get("user-agent")?.slice(0, 500),
  }).returning();
  return { session, token, csrfToken };
}

export async function revokeSession(sessionId: string) {
  await db.update(yeSessionsTable).set({ revokedAt: new Date() }).where(eq(yeSessionsTable.id, sessionId));
}

function readToken(req: Request): { token?: string; type: "web" | "mobile" | "impersonation" } {
  const impersonationToken = req.get(IMPERSONATION_TOKEN_HEADER);
  if (impersonationToken) {
    return { token: impersonationToken, type: "impersonation" };
  }
  const authorization = req.get("authorization");
  if (authorization?.toLowerCase().startsWith("bearer ")) {
    return { token: authorization.slice(7).trim(), type: "mobile" };
  }
  return { token: req.cookies?.[SESSION_COOKIE], type: "web" };
}

export async function authenticate(req: Request, res: Response, next: NextFunction) {
  try {
    const { token, type } = readToken(req);
    if (!token) {
      res.status(401).json({ error: "Autenticación requerida" });
      return;
    }
    const [session] = await db.select().from(yeSessionsTable).where(and(
      eq(yeSessionsTable.tokenHash, digest(token)),
      isNull(yeSessionsTable.revokedAt),
      gt(yeSessionsTable.expiresAt, new Date()),
    )).limit(1);
    if (!session || session.sessionType !== type) {
      res.status(401).json({ error: "Sesión inválida o expirada" });
      return;
    }
    const [user] = await db.select().from(yeUsersTable).where(and(eq(yeUsersTable.id, session.userId), eq(yeUsersTable.active, true))).limit(1);
    if (!user) {
      res.status(401).json({ error: "Usuario inactivo" });
      return;
    }
    await db.update(yeSessionsTable).set({ lastUsedAt: new Date() }).where(eq(yeSessionsTable.id, session.id));
    req.auth = { user, sessionId: session.id, sessionType: type, csrfToken: req.cookies?.[CSRF_COOKIE] };
    if ((type === "web" || type === "impersonation") && ["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
      const supplied = req.get("x-csrf-token");
      if (!supplied || !session.csrfTokenHash || digest(supplied) !== session.csrfTokenHash) {
        res.status(403).json({ error: "Token CSRF inválido" });
        return;
      }
    }
    next();
  } catch {
    res.status(500).json({ error: "No se pudo validar la sesión" });
  }
}

export function requireRoles(...roles: Role[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const role = req.auth?.user.role;
    if (!role || !roles.includes(role as Role)) {
      res.status(403).json({ error: "No tienes permisos para esta operación" });
      return;
    }
    next();
  };
}

export function setSessionCookies(res: Response, token: string, csrfToken?: string) {
  res.cookie(SESSION_COOKIE, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: SESSION_TTL_MS,
    path: "/",
  });
  if (csrfToken) {
    res.cookie(CSRF_COOKIE, csrfToken, {
      httpOnly: false,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      maxAge: SESSION_TTL_MS,
      path: "/",
    });
  }
}

export function clearSessionCookies(res: Response) {
  res.clearCookie(SESSION_COOKIE, { httpOnly: true, sameSite: "lax", path: "/" });
  res.clearCookie(CSRF_COOKIE, { httpOnly: false, sameSite: "lax", path: "/" });
}

export const publicSession = { SESSION_COOKIE, CSRF_COOKIE };