import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { randomInt } from 'node:crypto';
import { PrismaService } from '../infra/prisma.service';
import { SettingsService } from './settings.service';
import { MessagingService } from '../infra/messaging.service';
import { RedisService } from '../infra/redis.service';
import { NotificationsService } from './notifications.service';

@Injectable()
export class OtpService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly settings: SettingsService,
    private readonly messaging: MessagingService,
    private readonly redis: RedisService,
    private readonly notifications: NotificationsService
  ) {}

  async send(command: { target: string; channel: string; purpose: string; userId?: string; ipAddress?: string }) {
    const expiryMinutes = await this.settings.getNumber('OTP_EXPIRY_MINUTES', 10);
    const code = String(randomInt(100000, 999999));
    const expiresAt = new Date(Date.now() + expiryMinutes * 60_000);
    await this.prisma.authCode.create({
      data: {
        userId: command.userId,
        target: command.target,
        channel: command.channel,
        purpose: command.purpose,
        codeHash: await bcrypt.hash(code, 10),
        expiresAt
      }
    });
    const channel = command.channel === 'sms' ? 'sms' : 'email';
    await this.messaging.sendVerificationCode({
      channel,
      target: command.target,
      code,
      expiryMinutes
    });
    if (command.userId) {
      const ipLabel = command.ipAddress?.trim() || 'неизвестного IP';
      await this.notifications.create(
        command.userId,
        'otp_code',
        this.purposeLabel(command.purpose),
        `Попытка входа с ${ipLabel} — код для верификации: ${code}`,
        { code, ipAddress: command.ipAddress ?? null, purpose: command.purpose }
      );
    }
    await this.redis.client.del(this.attemptKey(command.target, command.purpose));
    return { sent: true, expiresAt: expiresAt.toISOString() };
  }

  async verify(command: { target: string; code: string; purpose: string }) {
    const authCode = await this.prisma.authCode.findFirst({
      where: {
        target: command.target,
        purpose: command.purpose,
        usedAt: null,
        expiresAt: { gt: new Date() }
      },
      orderBy: { createdAt: 'desc' }
    });

    if (!authCode) throw new BadRequestException('Код подтверждения не найден или истек');

    const attemptKey = this.attemptKey(command.target, command.purpose);
    const maxAttempts = await this.settings.getNumber('OTP_MAX_ATTEMPTS', 5);
    const attempts = Number((await this.redis.client.get(attemptKey)) ?? 0);
    if (attempts >= maxAttempts) {
      throw new UnauthorizedException('Превышено число попыток ввода кода');
    }

    const matches = await bcrypt.compare(command.code, authCode.codeHash);
    if (!matches) {
      await this.redis.client.set(attemptKey, String(attempts + 1), 'EX', 15 * 60);
      throw new UnauthorizedException('Неверный код подтверждения');
    }
    await this.redis.client.del(attemptKey);
    await this.prisma.authCode.update({ where: { id: authCode.id }, data: { usedAt: new Date() } });
    if (authCode.userId) {
      await this.notifications.dismissOtpCodeNotifications(authCode.userId);
    }
    return { verified: true };
  }

  private attemptKey(target: string, purpose: string) {
    return `otp:attempts:${purpose}:${target.toLowerCase()}`;
  }

  private purposeLabel(purpose: string) {
    switch (purpose) {
      case 'register':
        return 'Регистрация';
      case 'login':
        return 'Вход в аккаунт';
      case 'password-reset':
        return 'Сброс пароля';
      case 'password-change':
        return 'Смена пароля';
      default:
        return 'Подтверждение действия';
    }
  }
}
