import { prisma, tenantDb } from '../config/database.js';
import { hashPassword, verifyPassword } from '../auth/passwords.js';
import { generateAccessToken, generateRefreshToken, verifyToken } from '../auth/tokens.js';
import { AppError } from '../middlewares/error.middleware.js';
import { auditLog } from '../utils/audit.js';
import { getPlatformRoles } from '../utils/platform-admin.js';
import { getUserTenants, verifyMembership } from '../utils/memberships.js';
import { emailService } from './email/email.service.js';
import { env } from '../config/env.js';
import { invalidateTokenVersionCache } from '../middlewares/auth.middleware.js';
import type { LoginRequest, RegisterRequest, LoginResponse, Role } from '@horux/shared';
import { randomBytes } from 'crypto';

export async function register(data: RegisterRequest): Promise<LoginResponse> {
  const existingUser = await prisma.user.findUnique({
    where: { email: data.usuario.email.toLowerCase() },
  });

  if (existingUser) {
    throw new AppError(400, 'El email ya está registrado');
  }

  const existingTenant = await prisma.tenant.findUnique({
    where: { rfc: data.empresa.rfc },
  });

  if (existingTenant) {
    throw new AppError(400, 'El RFC ya está registrado');
  }

  // Provision a dedicated database for this tenant
  const databaseName = await tenantDb.provisionDatabase(data.empresa.rfc);

  const tenant = await prisma.tenant.create({
    data: {
      nombre: data.empresa.nombre,
      rfc: data.empresa.rfc.toUpperCase(),
      plan: 'trial',
      databaseName,
    },
  });

  const passwordHash = await hashPassword(data.usuario.password);
  const adminRol = await prisma.rol.findUnique({ where: { nombre: 'owner' } });
  if (!adminRol) throw new AppError(500, 'Rol admin no encontrado en catálogo');

  const user = await prisma.user.create({
    data: {
      email: data.usuario.email.toLowerCase(),
      passwordHash,
      nombre: data.usuario.nombre,
      lastTenantId: tenant.id,
    },
  });

  // Crea membership owner del caller en el tenant recién creado (fase 4 multi-tenant)
  await prisma.tenantMembership.create({
    data: {
      userId: user.id,
      tenantId: tenant.id,
      rolId: adminRol.id,
      isOwner: true,
      active: true,
    },
  });

  const ownerRole: Role = 'owner';
  const tokenPayload = {
    userId: user.id,
    email: user.email,
    role: ownerRole,
    tenantId: tenant.id,
  };

  const accessToken = generateAccessToken(tokenPayload);
  const refreshToken = generateRefreshToken(tokenPayload);

  await prisma.refreshToken.create({
    data: {
      userId: user.id,
      token: refreshToken,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  return {
    accessToken,
    refreshToken,
    user: {
      id: user.id,
      email: user.email,
      nombre: user.nombre,
      role: ownerRole,
      tenantId: tenant.id,
      tenantName: tenant.nombre,
      tenantRfc: tenant.rfc,
      plan: tenant.plan,
    },
  };
}

export async function login(data: LoginRequest): Promise<LoginResponse> {
  const user = await prisma.user.findUnique({
    where: { email: data.email.toLowerCase() },
  });

  if (!user) {
    throw new AppError(401, 'Credenciales inválidas');
  }

  if (!user.active) {
    throw new AppError(401, 'Usuario desactivado');
  }

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

  if (!isValidPassword) {
    throw new AppError(401, 'Credenciales inválidas');
  }

  // Resuelve el tenant activo desde memberships. Prefiere `lastTenantId` si
  // existe Y el user tiene membership activa ahí; sino cae al primer membership
  // por joinedAt ASC.
  const allMemberships = await prisma.tenantMembership.findMany({
    where: { userId: user.id, active: true, tenant: { active: true } },
    include: { tenant: true, rol: true },
    orderBy: { joinedAt: 'asc' },
  });

  let activeTenant;
  let activeRole: Role;

  if (allMemberships.length === 0) {
    // Edge case: user sin membership activa. Si tiene platformRoles (admin
    // global), permite login con cualquier tenant activo como nominal — su
    // trabajo real es vía impersonación desde /clientes. Sin esto, no podría
    // ni entrar a la plataforma para crear el primer cliente.
    const earlyPlatformRoles = await getPlatformRoles(user.id);
    if (earlyPlatformRoles.length === 0) {
      throw new AppError(401, 'No tienes acceso a ninguna empresa activa');
    }
    const fallbackTenant = await prisma.tenant.findFirst({
      where: { active: true },
      orderBy: { createdAt: 'asc' },
    });
    if (!fallbackTenant) {
      throw new AppError(503, 'No hay tenants activos en el sistema. Ejecuta `pnpm db:seed` para bootstrap.');
    }
    activeTenant = fallbackTenant;
    activeRole = 'visor' as Role; // mínimo — la autorización real viene de platformRoles
  } else {
    const preferred = user.lastTenantId
      ? allMemberships.find(m => m.tenantId === user.lastTenantId)
      : null;
    const activeMembership = preferred ?? allMemberships[0];
    activeTenant = activeMembership.tenant;
    activeRole = activeMembership.rol.nombre as Role;
  }

  // `loginCount` se incrementa SOLO en login (NO en refresh) — es la métrica
  // que dispara el auto-dismiss del onboarding tras N sesiones.
  const updatedUser = await prisma.user.update({
    where: { id: user.id },
    data: {
      lastLogin: new Date(),
      lastTenantId: activeTenant.id,
      loginCount: { increment: 1 },
    },
    select: { loginCount: true, onboardingDismissedAt: true },
  });

  auditLog({
    userId: user.id,
    tenantId: activeTenant.id,
    action: 'user.login',
    metadata: { email: user.email, tenantRfc: activeTenant.rfc },
  });

  const [platformRoles, tenants] = await Promise.all([
    getPlatformRoles(user.id),
    getUserTenants(user.id),
  ]);

  const tokenPayload = {
    userId: user.id,
    email: user.email,
    role: activeRole,
    tenantId: activeTenant.id,
    platformRoles,
    tokenVersion: user.tokenVersion,
  };

  const accessToken = generateAccessToken(tokenPayload);
  const refreshToken = generateRefreshToken(tokenPayload);

  await prisma.refreshToken.create({
    data: {
      userId: user.id,
      token: refreshToken,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  return {
    accessToken,
    refreshToken,
    user: {
      id: user.id,
      email: user.email,
      nombre: user.nombre,
      role: activeRole,
      tenantId: activeTenant.id,
      tenantName: activeTenant.nombre,
      tenantRfc: activeTenant.rfc,
      plan: activeTenant.plan,
      platformRoles,
      tenants,
      loginCount: updatedUser.loginCount,
      onboardingDismissedAt: updatedUser.onboardingDismissedAt,
    },
  };
}

export async function refreshTokens(token: string): Promise<{ accessToken: string; refreshToken: string }> {
  // Use a transaction to prevent race conditions
  return await prisma.$transaction(async (tx) => {
    const storedToken = await tx.refreshToken.findUnique({
      where: { token },
    });

    if (!storedToken) {
      throw new AppError(401, 'Token inválido');
    }

    if (storedToken.expiresAt < new Date()) {
      await tx.refreshToken.deleteMany({ where: { id: storedToken.id } });
      throw new AppError(401, 'Token expirado');
    }

    const payload = verifyToken(token);

    const user = await tx.user.findUnique({
      where: { id: payload.userId },
    });

    if (!user || !user.active) {
      throw new AppError(401, 'Usuario no encontrado o desactivado');
    }

    // Re-valida que el user sigue teniendo membership activa en el tenant del
    // JWT. Si lo removieron de ahí, cae al primer membership disponible.
    const currentMembership = await tx.tenantMembership.findFirst({
      where: { userId: user.id, tenantId: payload.tenantId, active: true, tenant: { active: true } },
      include: { tenant: true, rol: true },
    });
    let activeMembership = currentMembership;
    if (!activeMembership) {
      activeMembership = await tx.tenantMembership.findFirst({
        where: { userId: user.id, active: true, tenant: { active: true } },
        include: { tenant: true, rol: true },
        orderBy: { joinedAt: 'asc' },
      });
    }
    if (!activeMembership) {
      throw new AppError(401, 'No tienes acceso a ninguna empresa activa');
    }

    // Use deleteMany to avoid error if already deleted (race condition)
    await tx.refreshToken.deleteMany({ where: { id: storedToken.id } });

    const platformRoles = await getPlatformRoles(user.id);

    const newTokenPayload = {
      userId: user.id,
      email: user.email,
      role: activeMembership.rol.nombre as Role,
      tenantId: activeMembership.tenantId,
      platformRoles,
      tokenVersion: user.tokenVersion,
    };

    const accessToken = generateAccessToken(newTokenPayload);
    const refreshToken = generateRefreshToken(newTokenPayload);

    await tx.refreshToken.create({
      data: {
        userId: user.id,
        token: refreshToken,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      },
    });

    return { accessToken, refreshToken };
  });
}

export async function logout(token: string): Promise<void> {
  // Busca el refreshToken antes de borrarlo para capturar el userId en auditoría
  const rt = await prisma.refreshToken.findFirst({
    where: { token },
    select: { userId: true },
  });

  await prisma.refreshToken.deleteMany({
    where: { token },
  });

  if (rt) {
    const tenantId = (await prisma.user.findUnique({
      where: { id: rt.userId },
      select: { lastTenantId: true },
    }))?.lastTenantId ?? undefined;

    auditLog({
      userId: rt.userId,
      tenantId,
      action: 'user.logout',
    });
  }
}

// ============================================================================
// Password reset
// ============================================================================

const PASSWORD_RESET_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 horas

/**
 * Solicita recuperación de contraseña. No revela si el email existe (anti-enumeration).
 *
 * Si el email es válido y user activo:
 *   - Invalida cualquier token previo no usado del mismo user
 *   - Genera token criptográficamente seguro (32 bytes hex)
 *   - Envía email con link de reset (expira en 1h)
 *
 * Rate limit: aplicar en la capa de rutas (3/hora por IP).
 */
export async function requestPasswordReset(email: string): Promise<void> {
  const normalizedEmail = email.trim().toLowerCase();
  const user = await prisma.user.findUnique({
    where: { email: normalizedEmail },
    select: { id: true, email: true, nombre: true, active: true, lastTenantId: true },
  });

  // Respuesta idéntica para email existente/no-existente (anti-enumeration)
  if (!user || !user.active) {
    console.log(`[PasswordReset] Request para email inexistente o inactivo: ${normalizedEmail}`);
    return;
  }

  // Invalida tokens previos no usados (marca como usados)
  await prisma.passwordResetToken.updateMany({
    where: { userId: user.id, usedAt: null },
    data: { usedAt: new Date() },
  });

  const token = randomBytes(32).toString('hex');
  const expiresAt = new Date(Date.now() + PASSWORD_RESET_EXPIRY_MS);

  await prisma.passwordResetToken.create({
    data: { userId: user.id, token, expiresAt },
  });

  const resetUrl = `${env.FRONTEND_URL}/reset-password?token=${token}`;
  emailService.sendPasswordReset(user.email, { nombre: user.nombre, resetUrl })
    .catch(err => console.error('[EMAIL] Password reset notification failed:', err));

  auditLog({
    userId: user.id,
    tenantId: user.lastTenantId ?? undefined,
    action: 'user.password_reset_requested',
    metadata: { email: user.email },
  });

  console.log(`[PasswordReset] Token emitido para ${normalizedEmail}, expira ${expiresAt.toISOString()}`);
}

/**
 * Confirma recuperación de contraseña con token válido + nueva contraseña.
 *
 * Validaciones:
 *   - Password mínimo 8 caracteres
 *   - Token existe, no usado, no expirado
 *
 * Al éxito:
 *   - Actualiza password hash
 *   - Marca token como usado (single-use)
 *   - Borra todos los refresh tokens del user (invalida sesiones activas → re-login forzado)
 */
export async function confirmPasswordReset(token: string, newPassword: string): Promise<void> {
  if (!newPassword || newPassword.length < 8) {
    throw new AppError(400, 'La contraseña debe tener al menos 8 caracteres');
  }

  const record = await prisma.passwordResetToken.findUnique({
    where: { token },
    select: { id: true, userId: true, usedAt: true, expiresAt: true },
  });

  if (!record) throw new AppError(400, 'Token inválido');
  if (record.usedAt) throw new AppError(400, 'Este enlace ya fue usado. Solicita uno nuevo.');
  if (record.expiresAt < new Date()) throw new AppError(400, 'El enlace expiró. Solicita uno nuevo.');

  const passwordHash = await hashPassword(newPassword);

  await prisma.$transaction([
    // Actualiza hash + incrementa tokenVersion (invalida access tokens vivos)
    prisma.user.update({
      where: { id: record.userId },
      data: { passwordHash, tokenVersion: { increment: 1 } },
    }),
    prisma.passwordResetToken.update({
      where: { id: record.id },
      data: { usedAt: new Date() },
    }),
    // Invalida todos los refresh tokens activos del user
    prisma.refreshToken.deleteMany({
      where: { userId: record.userId },
    }),
  ]);

  // Propaga el incremento al cache del middleware (en todos los PM2 workers)
  invalidateTokenVersionCache(record.userId);

  const user = await prisma.user.findUnique({
    where: { id: record.userId },
    select: { lastTenantId: true, email: true },
  });

  auditLog({
    userId: record.userId,
    tenantId: user?.lastTenantId ?? undefined,
    action: 'user.password_reset_completed',
    metadata: { email: user?.email },
  });

  console.log(`[PasswordReset] Completado para user ${record.userId}`);
}

// ============================================================================
// Change password (authenticated) + Logout all
// ============================================================================

/**
 * Cambia la contraseña de un user autenticado. Requiere password actual para
 * prevenir cambios por alguien con acceso temporal a la sesión (ej: laptop
 * compartida dejada abierta). Incrementa tokenVersion — invalida TODAS las
 * sesiones activas del user (forza re-login en otros dispositivos).
 */
export async function changePassword(params: {
  userId: string;
  currentPassword: string;
  newPassword: string;
}): Promise<void> {
  if (params.newPassword.length < 8) {
    throw new AppError(400, 'La contraseña debe tener al menos 8 caracteres');
  }
  if (params.currentPassword === params.newPassword) {
    throw new AppError(400, 'La nueva contraseña debe ser distinta a la actual');
  }

  const user = await prisma.user.findUnique({
    where: { id: params.userId },
    select: { id: true, passwordHash: true, email: true, lastTenantId: true, active: true },
  });
  if (!user || !user.active) throw new AppError(401, 'Usuario no encontrado');

  const validCurrent = await verifyPassword(params.currentPassword, user.passwordHash);
  if (!validCurrent) throw new AppError(401, 'Contraseña actual incorrecta');

  const newHash = await hashPassword(params.newPassword);

  await prisma.$transaction([
    prisma.user.update({
      where: { id: user.id },
      data: { passwordHash: newHash, tokenVersion: { increment: 1 } },
    }),
    prisma.refreshToken.deleteMany({ where: { userId: user.id } }),
  ]);

  invalidateTokenVersionCache(user.id);

  auditLog({
    userId: user.id,
    tenantId: user.lastTenantId ?? undefined,
    action: 'user.password_changed',
    metadata: { email: user.email },
  });

  console.log(`[ChangePassword] Completado para user ${user.id}`);
}

/**
 * Cierra todas las sesiones activas del user. Usado por el botón
 * "Cerrar todas las sesiones" en /configuracion/seguridad. Incrementa
 * tokenVersion + borra refresh tokens. El user se queda sin acceso y debe
 * re-loguearse (incluyendo la sesión actual, por diseño — es lo que el
 * usuario pidió explícitamente).
 */
export async function logoutAllSessions(userId: string): Promise<void> {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { lastTenantId: true, email: true },
  });
  if (!user) throw new AppError(404, 'Usuario no encontrado');

  await prisma.$transaction([
    prisma.user.update({
      where: { id: userId },
      data: { tokenVersion: { increment: 1 } },
    }),
    prisma.refreshToken.deleteMany({ where: { userId } }),
  ]);

  invalidateTokenVersionCache(userId);

  auditLog({
    userId,
    tenantId: user.lastTenantId ?? undefined,
    action: 'user.sessions_invalidated',
    metadata: { email: user.email, reason: 'logout_all' },
  });

  console.log(`[LogoutAll] Sesiones invalidadas para user ${userId}`);
}

// ============================================================================
// Onboarding dismiss
// ============================================================================

/**
 * Marca el onboarding como dismissed. Idempotente — si ya estaba seteado, no
 * sobrescribe el timestamp original (preserva la fecha del primer dismiss).
 */
export async function dismissOnboarding(userId: string): Promise<{ onboardingDismissedAt: Date }> {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { onboardingDismissedAt: true },
  });
  if (!user) throw new AppError(404, 'Usuario no encontrado');

  if (user.onboardingDismissedAt) {
    return { onboardingDismissedAt: user.onboardingDismissedAt };
  }

  const updated = await prisma.user.update({
    where: { id: userId },
    data: { onboardingDismissedAt: new Date() },
    select: { onboardingDismissedAt: true },
  });
  return { onboardingDismissedAt: updated.onboardingDismissedAt! };
}

// ============================================================================
// Switch tenant (multi-membership)
// ============================================================================

/**
 * Cambia el tenant activo del user. Valida que tenga membership activa en el
 * tenant destino, luego emite un nuevo par de tokens apuntando a ese tenantId
 * (con el rol que tiene en ese tenant específico). El refresh token actual se
 * invalida — el user opera con el par nuevo desde este momento.
 *
 * Casos de uso:
 *   - Owner con varias empresas cambia entre ellas
 *   - Contador que atiende múltiples clientes cambia de empresa activa
 *
 * Un user con 1 sola membership no debería llamarlo (no cambia nada), pero si
 * lo hace funciona igual: le da tokens nuevos apuntando al mismo tenant.
 */
export async function switchTenant(params: {
  userId: string;
  currentRefreshToken: string;
  targetTenantId: string;
}): Promise<LoginResponse> {
  const membership = await verifyMembership(params.userId, params.targetTenantId);
  if (!membership) {
    throw new AppError(403, 'No tienes acceso a esa empresa');
  }

  const user = await prisma.user.findUnique({
    where: { id: params.userId },
  });
  if (!user || !user.active) throw new AppError(401, 'Usuario no encontrado');

  const targetTenant = await prisma.tenant.findUnique({
    where: { id: params.targetTenantId },
  });
  if (!targetTenant || !targetTenant.active) {
    throw new AppError(404, 'Empresa no encontrada o desactivada');
  }

  const [platformRoles, tenants] = await Promise.all([
    getPlatformRoles(user.id),
    getUserTenants(user.id),
  ]);

  const tokenPayload = {
    userId: user.id,
    email: user.email,
    role: membership.rolNombre,
    tenantId: targetTenant.id,
    platformRoles,
    tokenVersion: user.tokenVersion,
  };

  const accessToken = generateAccessToken(tokenPayload);
  const refreshToken = generateRefreshToken(tokenPayload);

  // Persiste el target como "último tenant activo" y atomiza la rotacion del
  // refresh token (delete + create) para evitar race conditions con requests
  // concurrentes que intenten refrescar con el token anterior.
  const previousTenantId = user.lastTenantId;
  await prisma.$transaction([
    prisma.user.update({
      where: { id: user.id },
      data: { lastTenantId: targetTenant.id },
    }),
    // Invalida el refresh token actual (puede no existir si el caller pasó el
    // access token por error — deleteMany es idempotente).
    prisma.refreshToken.deleteMany({ where: { token: params.currentRefreshToken } }),
    prisma.refreshToken.create({
      data: {
        userId: user.id,
        token: refreshToken,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      },
    }),
  ]);

  auditLog({
    userId: user.id,
    tenantId: targetTenant.id,
    action: 'user.tenant_switched',
    metadata: { from: previousTenantId ?? null, to: targetTenant.id, targetRfc: targetTenant.rfc },
  });

  return {
    accessToken,
    refreshToken,
    user: {
      id: user.id,
      email: user.email,
      nombre: user.nombre,
      role: membership.rolNombre,
      tenantId: targetTenant.id,
      tenantName: targetTenant.nombre,
      tenantRfc: targetTenant.rfc,
      plan: targetTenant.plan,
      platformRoles,
      tenants,
    },
  };
}
