import {
  Injectable,
  UnauthorizedException,
  NotFoundException,
  BadRequestException,
  Logger,
} from '@nestjs/common';
import { authenticator } from 'otplib';
import * as bcrypt from 'bcryptjs';
import { randomBytes } from 'crypto';
import { PrismaService } from '@chatapp/database';

@Injectable()
export class TwoFactorService {
  private readonly logger = new Logger(TwoFactorService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Genera un secret TOTP, lo guarda en la BD (enabled=false)
   * y devuelve el secret + la URL del QR code.
   */
  async generateSecret(userId: string): Promise<{ secret: string; qrUrl: string }> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('User not found');
    }

    const secret = authenticator.generateSecret();
    const qrUrl = `otpauth://totp/ChatApp:${encodeURIComponent(user.email)}?secret=${secret}&issuer=ChatApp`;

    // Si ya existe un registro, lo actualizamos; si no, lo creamos
    const existing = await this.prisma.twoFactorAuth.findUnique({
      where: { userId },
    });

    if (existing) {
      await this.prisma.twoFactorAuth.update({
        where: { userId },
        data: { secret, enabled: false, enabledAt: null, backupCodes: undefined },
      });
    } else {
      await this.prisma.twoFactorAuth.create({
        data: { userId, secret, enabled: false },
      });
    }

    return { secret, qrUrl };
  }

  /**
   * Verifica el codigo TOTP y, si es correcto, activa 2FA y genera backup codes.
   */
  async verifyAndEnable(
    userId: string,
    token: string,
  ): Promise<{ backupCodes: string[] }> {
    const record = await this.prisma.twoFactorAuth.findUnique({
      where: { userId },
    });
    if (!record) {
      throw new BadRequestException('2FA not initialized. Call enable first.');
    }
    if (record.enabled) {
      throw new BadRequestException('2FA is already enabled');
    }

    const isValid = authenticator.verify({ token, secret: record.secret });
    if (!isValid) {
      throw new UnauthorizedException('Invalid TOTP code');
    }

    const backupCodes = this.generateBackupCodes();

    await this.prisma.twoFactorAuth.update({
      where: { userId },
      data: {
        enabled: true,
        enabledAt: new Date(),
        backupCodes: backupCodes,
      },
    });

    return { backupCodes };
  }

  /**
   * Verifica un codigo TOTP (para login con 2FA).
   * Tambien acepta backup codes.
   */
  async verifyToken(userId: string, token: string): Promise<boolean> {
    const record = await this.prisma.twoFactorAuth.findUnique({
      where: { userId },
    });
    if (!record || !record.enabled) {
      throw new BadRequestException('2FA not enabled');
    }

    // Primero intentar como backup code
    const backupCodes = (record.backupCodes as string[]) ?? [];
    if (backupCodes.includes(token)) {
      // Consumir el backup code
      const remaining = backupCodes.filter((c) => c !== token);
      await this.prisma.twoFactorAuth.update({
        where: { userId },
        data: { backupCodes: remaining },
      });
      return true;
    }

    // Si no es backup code, verificar como TOTP
    const isValid = authenticator.verify({ token, secret: record.secret });
    if (!isValid) {
      throw new UnauthorizedException('Invalid TOTP code');
    }

    return true;
  }

  /**
   * Desactiva 2FA verificando que el usuario conozca su password.
   */
  async disable(userId: string, password: string): Promise<{ message: string }> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('User not found');
    }

    const valid = await bcrypt.compare(password, user.passwordHash || '');
    if (!valid) {
      throw new UnauthorizedException('Invalid password');
    }

    await this.prisma.twoFactorAuth.deleteMany({ where: { userId } });

    return { message: '2FA disabled successfully' };
  }

  /**
   * Genera nuevos backup codes (solo si 2FA esta habilitado).
   */
  async regenerateBackupCodes(userId: string): Promise<{ backupCodes: string[] }> {
    const record = await this.prisma.twoFactorAuth.findUnique({
      where: { userId },
    });
    if (!record || !record.enabled) {
      throw new BadRequestException('2FA not enabled');
    }

    const backupCodes = this.generateBackupCodes();

    await this.prisma.twoFactorAuth.update({
      where: { userId },
      data: { backupCodes },
    });

    return { backupCodes };
  }

  /**
   * Devuelve los backup codes actuales.
   */
  async getBackupCodes(userId: string): Promise<{ backupCodes: string[] }> {
    const record = await this.prisma.twoFactorAuth.findUnique({
      where: { userId },
    });
    if (!record || !record.enabled) {
      throw new BadRequestException('2FA not enabled');
    }

    return { backupCodes: (record.backupCodes as string[]) ?? [] };
  }

  /**
   * Devuelve si 2FA esta activado para un usuario.
   */
  async getStatus(userId: string): Promise<{ enabled: boolean }> {
    const record = await this.prisma.twoFactorAuth.findUnique({
      where: { userId },
    });
    return { enabled: record?.enabled ?? false };
  }

  /**
   * Genera 10 codigos aleatorios de 8 digitos.
   */
  private generateBackupCodes(): string[] {
    const codes: string[] = [];
    for (let i = 0; i < 10; i++) {
      const buf = randomBytes(4);
      const code = parseInt(buf.toString('hex'), 16)
        .toString()
        .padStart(8, '0')
        .slice(-8);
      codes.push(code);
    }
    return codes;
  }
}