// templates/auth.service.ts.hbs
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { PrismaClient, User, RefreshToken } from '@prisma/client';
import { EmailService } from '../email/email.service';
import { AuditService } from '../audit/audit.service';
import crypto from 'crypto';

interface LoginCredentials {
  email: string;
  password: string;
}

interface RegisterData {
  email: string;
  password: string;
  firstName?: string;
  lastName?: string;
  role?: 'ADMIN' | 'MANAGER' | 'EMPLOYEE';
}

interface AuthTokens {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

interface JwtPayload {
  userId: string;
  email: string;
  role: string;
  iat?: number;
  exp?: number;
}

export class AuthService {
  private prisma: PrismaClient;
  private emailService: EmailService;
  private auditService: AuditService;
  private jwtSecret: string;
  private jwtExpiresIn: string;
  private refreshExpiresIn: string;
  private bcryptRounds: number;

  constructor() {
    this.prisma = new PrismaClient();
    this.emailService = new EmailService();
    this.auditService = new AuditService();
    this.jwtSecret = process.env.JWT_SECRET!;
    this.jwtExpiresIn = process.env.JWT_EXPIRES_IN || '15m';
    this.refreshExpiresIn = process.env.JWT_REFRESH_EXPIRES_IN || '30d';
    this.bcryptRounds = parseInt(process.env.BCRYPT_ROUNDS || '12');

    if (!this.jwtSecret) {
      throw new Error('JWT_SECRET environment variable is required');
    }
  }

  /**
   * Registrar un nuevo usuario
   */
  async register(userData: RegisterData): Promise<{ user: Omit<User, 'password'>; tokens: AuthTokens }> {
    try {
      // Verificar si el usuario ya existe
      const existingUser = await this.prisma.user.findUnique({
        where: { email: userData.email }
      });

      if (existingUser) {
        throw new Error('El usuario ya existe con este email');
      }

      // Validar contraseña
      this.validatePassword(userData.password);

      // Hash de la contraseña
      const hashedPassword = await bcrypt.hash(userData.password, this.bcryptRounds);

      // Crear usuario
      const user = await this.prisma.user.create({
        data: {
          email: userData.email,
          password: hashedPassword,
          firstName: userData.firstName,
          lastName: userData.lastName,
          role: userData.role || 'EMPLOYEE',
          isEmailVerified: false,
          isActive: true
        }
      });

      // Generar tokens
      const tokens = await this.generateTokens(user);

      // Enviar email de verificación
      {{#if features.email-verification}}
      await this.sendVerificationEmail(user.email);
      {{/if}}

      // Registrar en auditoría
      await this.auditService.log({
        userId: user.id,
        action: 'USER_REGISTERED',
        details: { email: user.email, role: user.role }
      });

      // Remover password del resultado
      const { password, ...userWithoutPassword } = user;

      return {
        user: userWithoutPassword,
        tokens
      };
    } catch (error) {
      await this.auditService.log({
        action: 'REGISTRATION_FAILED',
        details: { email: userData.email, error: error.message }
      });
      throw error;
    }
  }

  /**
   * Iniciar sesión
   */
  async login(credentials: LoginCredentials, ipAddress?: string): Promise<{ user: Omit<User, 'password'>; tokens: AuthTokens }> {
    try {
      // Buscar usuario
      const user = await this.prisma.user.findUnique({
        where: { email: credentials.email }
      });

      if (!user) {
        throw new Error('Credenciales inválidas');
      }

      // Verificar si el usuario está activo
      if (!user.isActive) {
        throw new Error('Cuenta desactivada');
      }

      // Verificar contraseña
      const isPasswordValid = await bcrypt.compare(credentials.password, user.password);
      if (!isPasswordValid) {
        await this.auditService.log({
          userId: user.id,
          action: 'LOGIN_FAILED',
          details: { email: user.email, reason: 'invalid_password', ipAddress }
        });
        throw new Error('Credenciales inválidas');
      }

      // Actualizar último login
      await this.prisma.user.update({
        where: { id: user.id },
        data: { lastLoginAt: new Date() }
      });

      // Generar tokens
      const tokens = await this.generateTokens(user);

      // Registrar login exitoso
      await this.auditService.log({
        userId: user.id,
        action: 'USER_LOGIN',
        details: { email: user.email, ipAddress }
      });

      // Remover password del resultado
      const { password, ...userWithoutPassword } = user;

      return {
        user: userWithoutPassword,
        tokens
      };
    } catch (error) {
      await this.auditService.log({
        action: 'LOGIN_FAILED',
        details: { email: credentials.email, error: error.message }
      });
      throw error;
    }
  }

  /**
   * Renovar tokens usando refresh token
   */
  async refreshTokens(refreshToken: string): Promise<AuthTokens> {
    try {
      // Buscar refresh token
      const storedToken = await this.prisma.refreshToken.findUnique({
        where: { token: refreshToken },
        include: { user: true }
      });

      if (!storedToken || storedToken.isRevoked || storedToken.expiresAt < new Date()) {
        throw new Error('Refresh token inválido o expirado');
      }

      // Verificar que el usuario esté activo
      if (!storedToken.user.isActive) {
        throw new Error('Usuario desactivado');
      }

      // Revocar el token actual
      await this.prisma.refreshToken.update({
        where: { id: storedToken.id },
        data: { isRevoked: true }
      });

      // Generar nuevos tokens
      const tokens = await this.generateTokens(storedToken.user);

      await this.auditService.log({
        userId: storedToken.user.id,
        action: 'TOKENS_REFRESHED',
        details: { email: storedToken.user.email }
      });

      return tokens;
    } catch (error) {
      await this.auditService.log({
        action: 'TOKEN_REFRESH_FAILED',
        details: { error: error.message }
      });
      throw error;
    }
  }

  /**
   * Cerrar sesión
   */
  async logout(refreshToken: string): Promise<void> {
    try {
      const storedToken = await this.prisma.refreshToken.findUnique({
        where: { token: refreshToken },
        include: { user: true }
      });

      if (storedToken) {
        await this.prisma.refreshToken.update({
          where: { id: storedToken.id },
          data: { isRevoked: true }
        });

        await this.auditService.log({
          userId: storedToken.user.id,
          action: 'USER_LOGOUT',
          details: { email: storedToken.user.email }
        });
      }
    } catch (error) {
      await this.auditService.log({
        action: 'LOGOUT_FAILED',
        details: { error: error.message }
      });
      throw error;
    }
  }

  {{#if features.password-reset}}
  /**
   * Solicitar restablecimiento de contraseña
   */
  async forgotPassword(email: string): Promise<void> {
    try {
      const user = await this.prisma.user.findUnique({
        where: { email }
      });

      if (!user) {
        // No revelar si el email existe o no
        return;
      }

      // Generar token de reset
      const resetToken = crypto.randomBytes(32).toString('hex');
      const expiresAt = new Date(Date.now() + 3600000); // 1 hora

      // Guardar token de reset
      await this.prisma.passwordReset.create({
        data: {
          email,
          token: resetToken,
          expiresAt
        }
      });

      // Enviar email de reset
      await this.emailService.sendPasswordResetEmail(email, resetToken);

      await this.auditService.log({
        userId: user.id,
        action: 'PASSWORD_RESET_REQUESTED',
        details: { email }
      });
    } catch (error) {
      await this.auditService.log({
        action: 'PASSWORD_RESET_FAILED',
        details: { email, error: error.message }
      });
      throw error;
    }
  }

  /**
   * Restablecer contraseña
   */
  async resetPassword(token: string, newPassword: string): Promise<void> {
    try {
      // Buscar token de reset
      const resetRequest = await this.prisma.passwordReset.findUnique({
        where: { token }
      });

      if (!resetRequest || resetRequest.isUsed || resetRequest.expiresAt < new Date()) {
        throw new Error('Token de reset inválido o expirado');
      }

      // Validar nueva contraseña
      this.validatePassword(newPassword);

      // Hash de la nueva contraseña
      const hashedPassword = await bcrypt.hash(newPassword, this.bcryptRounds);

      // Actualizar contraseña
      await this.prisma.user.update({
        where: { email: resetRequest.email },
        data: { password: hashedPassword }
      });

      // Marcar token como usado
      await this.prisma.passwordReset.update({
        where: { id: resetRequest.id },
        data: { isUsed: true }
      });

      // Revocar todos los refresh tokens del usuario
      await this.prisma.refreshToken.updateMany({
        where: { user: { email: resetRequest.email } },
        data: { isRevoked: true }
      });

      const user = await this.prisma.user.findUnique({
        where: { email: resetRequest.email }
      });

      await this.auditService.log({
        userId: user?.id,
        action: 'PASSWORD_RESET_COMPLETED',
        details: { email: resetRequest.email }
      });
    } catch (error) {
      await this.auditService.log({
        action: 'PASSWORD_RESET_FAILED',
        details: { error: error.message }
      });
      throw error;
    }
  }
  {{/if}}

  {{#if features.email-verification}}
  /**
   * Enviar email de verificación
   */
  async sendVerificationEmail(email: string): Promise<void> {
    const verificationToken = crypto.randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + 86400000); // 24 horas

    await this.prisma.emailVerification.create({
      data: {
        email,
        token: verificationToken,
        expiresAt
      }
    });

    await this.emailService.sendVerificationEmail(email, verificationToken);
  }

  /**
   * Verificar email
   */
  async verifyEmail(token: string): Promise<void> {
    const verification = await this.prisma.emailVerification.findUnique({
      where: { token }
    });

    if (!verification || verification.isUsed || verification.expiresAt < new Date()) {
      throw new Error('Token de verificación inválido o expirado');
    }

    await this.prisma.user.update({
      where: { email: verification.email },
      data: { isEmailVerified: true }
    });

    await this.prisma.emailVerification.update({
      where: { id: verification.id },
      data: { isUsed: true }
    });
  }
  {{/if}}

  /**
   * Verificar token JWT
   */
  async verifyToken(token: string): Promise<JwtPayload> {
    try {
      const decoded = jwt.verify(token, this.jwtSecret) as JwtPayload;
      
      // Verificar que el usuario aún existe y está activo
      const user = await this.prisma.user.findUnique({
        where: { id: decoded.userId }
      });

      if (!user || !user.isActive) {
        throw new Error('Usuario no válido');
      }

      return decoded;
    } catch (error) {
      throw new Error('Token inválido');
    }
  }

  /**
   * Generar tokens de acceso y refresh
   */
  private async generateTokens(user: User): Promise<AuthTokens> {
    const payload: JwtPayload = {
      userId: user.id,
      email: user.email,
      role: user.role
    };

    const accessToken = jwt.sign(payload, this.jwtSecret, {
      expiresIn: this.jwtExpiresIn,
      issuer: process.env.JWT_ISSUER,
      audience: process.env.JWT_AUDIENCE
    });

    const refreshToken = crypto.randomBytes(64).toString('hex');
    const refreshExpiresAt = new Date();
    refreshExpiresAt.setDate(refreshExpiresAt.getDate() + 30); // 30 días

    // Guardar refresh token
    await this.prisma.refreshToken.create({
      data: {
        token: refreshToken,
        userId: user.id,
        expiresAt: refreshExpiresAt
      }
    });

    // Limpiar tokens expirados
    await this.cleanupExpiredTokens(user.id);

    return {
      accessToken,
      refreshToken,
      expiresIn: this.parseExpirationTime(this.jwtExpiresIn)
    };
  }

  /**
   * Validar contraseña
   */
  private validatePassword(password: string): void {
    if (password.length < 8) {
      throw new Error('La contraseña debe tener al menos 8 caracteres');
    }

    if (!/(?=.*[a-z])/.test(password)) {
      throw new Error('La contraseña debe contener al menos una letra minúscula');
    }

    if (!/(?=.*[A-Z])/.test(password)) {
      throw new Error('La contraseña debe contener al menos una letra mayúscula');
    }

    if (!/(?=.*\d)/.test(password)) {
      throw new Error('La contraseña debe contener al menos un número');
    }

    if (!/(?=.*[@$!%*?&])/.test(password)) {
      throw new Error('La contraseña debe contener al menos un carácter especial');
    }
  }

  /**
   * Limpiar tokens expirados
   */
  private async cleanupExpiredTokens(userId: string): Promise<void> {
    await this.prisma.refreshToken.deleteMany({
      where: {
        userId,
        OR: [
          { expiresAt: { lt: new Date() } },
          { isRevoked: true }
        ]
      }
    });
  }

  /**
   * Parsear tiempo de expiración a segundos
   */
  private parseExpirationTime(timeString: string): number {
    const unit = timeString.slice(-1);
    const value = parseInt(timeString.slice(0, -1));

    switch (unit) {
      case 's': return value;
      case 'm': return value * 60;
      case 'h': return value * 3600;
      case 'd': return value * 86400;
      default: return 900; // 15 minutos por defecto
    }
  }

  /**
   * Obtener perfil del usuario
   */
  async getUserProfile(userId: string): Promise<Omit<User, 'password'>> {
    const user = await this.prisma.user.findUnique({
      where: { id: userId }
    });

    if (!user) {
      throw new Error('Usuario no encontrado');
    }

    const { password, ...userProfile } = user;
    return userProfile;
  }

  /**
   * Actualizar perfil del usuario
   */
  async updateUserProfile(userId: string, updateData: Partial<Pick<User, 'firstName' | 'lastName'>>): Promise<Omit<User, 'password'>> {
    const user = await this.prisma.user.update({
      where: { id: userId },
      data: updateData
    });

    await this.auditService.log({
      userId,
      action: 'PROFILE_UPDATED',
      details: updateData
    });

    const { password, ...userProfile } = user;
    return userProfile;
  }
}