import "server-only";
import { db } from "@/server/db";
import { writeAudit } from "@/server/audit";
import { logger } from "@/server/logger";
import { AUTH_CONFIG } from "@/server/auth/config";
import { getDummyHash, verifyPassword } from "@/server/auth/password";
import { createSession, revokeSessionByToken } from "@/server/auth/session-service";

export type LoginRequest = {
  /** Ya normalizado (minúsculas) por loginSchema. */
  email: string;
  password: string;
  ip: string;
  userAgent: string | null;
  /** Token de la cookie actual, si existe: se revoca para regenerar la sesión. */
  currentToken?: string | null;
};

export type LoginFailureCode = "INVALID_CREDENTIALS" | "RATE_LIMITED";

export type LoginResult =
  | {
      ok: true;
      token: string;
      expiresAt: Date;
      userId: string;
      mustChangePassword: boolean;
    }
  | { ok: false; code: LoginFailureCode };

/** Mensajes para el usuario: nunca revelan si el email existe o si la cuenta está bloqueada. */
export const LOGIN_ERROR_MESSAGES: Record<LoginFailureCode, string> = {
  INVALID_CREDENTIALS:
    "No pudimos iniciar sesión con esos datos. Si fallaste varias veces, espera unos minutos e inténtalo de nuevo.",
  RATE_LIMITED: "Demasiados intentos. Espera unos minutos antes de volver a intentarlo.",
};

async function recordAttempt(
  email: string,
  ip: string,
  success: boolean,
  reason: string | null,
): Promise<void> {
  await db.loginAttempt.create({ data: { email, ip, success, reason } });
}

async function isRateLimited(email: string, ip: string, now: Date): Promise<boolean> {
  const since = new Date(now.getTime() - AUTH_CONFIG.rateLimitWindowMs);
  const [byIp, byEmail] = await Promise.all([
    ip === "unknown"
      ? Promise.resolve(0)
      : db.loginAttempt.count({ where: { ip, success: false, createdAt: { gte: since } } }),
    db.loginAttempt.count({ where: { email, success: false, createdAt: { gte: since } } }),
  ]);
  return (
    byIp >= AUTH_CONFIG.maxFailedAttemptsPerIp || byEmail >= AUTH_CONFIG.maxFailedAttemptsPerEmail
  );
}

export async function login(request: LoginRequest, now: Date = new Date()): Promise<LoginResult> {
  const { email, password, ip, userAgent } = request;
  const meta = { ip, userAgent };

  if (await isRateLimited(email, ip, now)) {
    await recordAttempt(email, ip, false, "rate_limited");
    logger.warn({ ip }, "login rate limited");
    return { ok: false, code: "RATE_LIMITED" };
  }

  const user = await db.user.findUnique({
    where: { email },
    select: {
      id: true,
      email: true,
      passwordHash: true,
      isActive: true,
      deletedAt: true,
      lockedUntil: true,
      mustChangePassword: true,
    },
  });

  if (!user || user.deletedAt || !user.isActive) {
    // Igualar el tiempo de respuesta con el caso "usuario existe".
    await verifyPassword(await getDummyHash(), password);
    await recordAttempt(email, ip, false, user ? "inactive_user" : "unknown_email");
    if (user) {
      await writeAudit({
        userId: user.id,
        action: "auth.login_failed",
        entity: "user",
        entityId: user.id,
        summary: `Intento de login de usuario inactivo (${user.email})`,
        ...meta,
      });
    }
    return { ok: false, code: "INVALID_CREDENTIALS" };
  }

  if (user.lockedUntil && user.lockedUntil > now) {
    await verifyPassword(await getDummyHash(), password);
    await recordAttempt(email, ip, false, "account_locked");
    await writeAudit({
      userId: user.id,
      action: "auth.login_blocked",
      entity: "user",
      entityId: user.id,
      summary: `Intento de login con la cuenta bloqueada (${user.email})`,
      ...meta,
    });
    return { ok: false, code: "INVALID_CREDENTIALS" };
  }

  const passwordOk = await verifyPassword(user.passwordHash, password);

  if (!passwordOk) {
    const updated = await db.user.update({
      where: { id: user.id },
      data: { failedLoginCount: { increment: 1 } },
      select: { failedLoginCount: true },
    });
    const mustLock = updated.failedLoginCount >= AUTH_CONFIG.maxFailedAttemptsPerAccount;
    if (mustLock) {
      await db.user.update({
        where: { id: user.id },
        data: {
          failedLoginCount: 0,
          lockedUntil: new Date(now.getTime() + AUTH_CONFIG.lockDurationMs),
        },
      });
    }
    await recordAttempt(email, ip, false, "wrong_password");
    await writeAudit({
      userId: user.id,
      action: mustLock ? "auth.account_locked" : "auth.login_failed",
      entity: "user",
      entityId: user.id,
      summary: mustLock
        ? `Cuenta bloqueada temporalmente tras ${AUTH_CONFIG.maxFailedAttemptsPerAccount} intentos fallidos (${user.email})`
        : `Contraseña incorrecta (${user.email})`,
      ...meta,
    });
    return { ok: false, code: "INVALID_CREDENTIALS" };
  }

  // Éxito: regenerar la sesión (la anterior, si había, se revoca).
  if (request.currentToken) {
    await revokeSessionByToken(request.currentToken, "rotated_on_login", now);
  }

  const session = await db.$transaction(async (tx) => {
    await tx.user.update({
      where: { id: user.id },
      data: { failedLoginCount: 0, lockedUntil: null, lastLoginAt: now, lastLoginIp: ip },
    });
    const created = await createSession(user.id, meta, now, tx);
    await writeAudit(
      {
        userId: user.id,
        action: "auth.login",
        entity: "session",
        entityId: created.sessionId,
        summary: `Inicio de sesión (${user.email})`,
        ...meta,
      },
      tx,
    );
    return created;
  });

  await recordAttempt(email, ip, true, null);

  return {
    ok: true,
    token: session.token,
    expiresAt: session.expiresAt,
    userId: user.id,
    mustChangePassword: user.mustChangePassword,
  };
}

export async function logout(
  token: string,
  meta: { ip: string; userAgent: string | null },
  now: Date = new Date(),
): Promise<void> {
  const revoked = await revokeSessionByToken(token, "logout", now);
  if (!revoked) return;
  await writeAudit({
    userId: revoked.userId,
    action: "auth.logout",
    entity: "session",
    entityId: revoked.sessionId,
    summary: "Cierre de sesión",
    ...meta,
  });
}
