import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { SessionStatus } from '../generated/prisma/client';
import { PrismaService } from '../infra/prisma.service';
import { AuthService } from './auth.service';
import { SessionService } from './session.service';
import { SettingsService } from './settings.service';

@Injectable()
export class PinService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly auth: AuthService,
    private readonly settings: SettingsService,
    private readonly session: SessionService
  ) {}

  private async getPinLengthBounds() {
    const min = await this.settings.getNumber('PIN_MIN_LENGTH', 4);
    const max = await this.settings.getNumber('PIN_MAX_LENGTH', 6);
    return { min, max };
  }

  private async assertValidPinFormat(pin: string) {
    const { min, max } = await this.getPinLengthBounds();
    const pattern = new RegExp(`^\\d{${min},${max}}$`);
    if (!pattern.test(pin)) {
      throw new BadRequestException(`PIN-код должен содержать от ${min} до ${max} цифр`);
    }
  }

  async enablePin(userId: string, pin: string) {
    await this.assertValidPinFormat(pin);

    return this.prisma.pinCode.upsert({
      where: { userId },
      create: {
        userId,
        hash: await bcrypt.hash(pin, 12),
        isEnabled: true,
        deletionRequestedAt: null
      },
      update: {
        hash: await bcrypt.hash(pin, 12),
        isEnabled: true,
        deletionRequestedAt: null
      }
    });
  }

  async setupPin(userId: string, pin: string) {
    const pinCode = await this.enablePin(userId, pin);
    return { userId: pinCode.userId, isEnabled: pinCode.isEnabled, deletionRequestedAt: pinCode.deletionRequestedAt?.toISOString() };
  }

  async updatePin(userId: string, pin: string) {
    const pinCode = await this.enablePin(userId, pin);
    await this.prisma.session.updateMany({
      where: { userId, status: SessionStatus.ACTIVE },
      data: { pinVerified: false, status: SessionStatus.LOCKED }
    });
    return { userId: pinCode.userId, isEnabled: pinCode.isEnabled, deletionRequestedAt: pinCode.deletionRequestedAt?.toISOString() };
  }

  async requestPinDeletion(userId: string, pin?: string) {
    const pinCode = await this.prisma.pinCode.findUnique({ where: { userId } });
    if (!pinCode?.isEnabled) {
      throw new BadRequestException('PIN-код не включен');
    }

    const requirePin = await this.settings.getBoolean('PIN_REQUIRE_ON_DELETE', true);
    if (requirePin) {
      if (!pin) {
        throw new BadRequestException('Для удаления PIN-кода требуется подтверждение текущим PIN');
      }
      const matches = await bcrypt.compare(pin, pinCode.hash);
      if (!matches) {
        throw new UnauthorizedException('Неверный PIN-код');
      }
    }

    const graceMinutes = await this.settings.getNumber('PIN_DELETE_GRACE_MINUTES', 1440);
    const deletionRequestedAt = new Date();
    const effectiveAt = new Date(deletionRequestedAt.getTime() + graceMinutes * 60_000);

    await this.prisma.pinCode.update({
      where: { userId },
      data: { deletionRequestedAt }
    });

    return {
      userId,
      deletionRequestedAt: deletionRequestedAt.toISOString(),
      effectiveAt: effectiveAt.toISOString(),
      graceMinutes
    };
  }

  async cancelPinDeletion(userId: string) {
    const pinCode = await this.prisma.pinCode.findUnique({ where: { userId } });
    if (!pinCode) {
      throw new BadRequestException('PIN-код не найден');
    }
    await this.prisma.pinCode.update({
      where: { userId },
      data: { deletionRequestedAt: null }
    });
    return { userId, cancelled: true };
  }

  async finalizeDuePinDeletions() {
    const graceMinutes = await this.settings.getNumber('PIN_DELETE_GRACE_MINUTES', 1440);
    const threshold = new Date(Date.now() - graceMinutes * 60_000);
    const due = await this.prisma.pinCode.findMany({
      where: {
        isEnabled: true,
        deletionRequestedAt: { lte: threshold }
      }
    });

    for (const pinCode of due) {
      await this.disablePin(pinCode.userId, { skipGraceCheck: true });
    }

    return { count: due.length };
  }

  async disablePin(userId: string, options?: { skipGraceCheck?: boolean }) {
    const pinCode = await this.prisma.pinCode.findUnique({ where: { userId } });
    if (!pinCode?.isEnabled) {
      throw new BadRequestException('PIN-код не включен');
    }

    if (!options?.skipGraceCheck && pinCode.deletionRequestedAt) {
      const graceMinutes = await this.settings.getNumber('PIN_DELETE_GRACE_MINUTES', 1440);
      const effectiveAt = pinCode.deletionRequestedAt.getTime() + graceMinutes * 60_000;
      if (Date.now() < effectiveAt) {
        throw new BadRequestException('PIN-код будет удалён после истечения периода ожидания');
      }
    }

    return this.prisma.pinCode.update({
      where: { userId },
      data: { isEnabled: false, deletionRequestedAt: null }
    });
  }

  async verifyPin(sessionId: string, pin: string) {
    const session = await this.prisma.session.findUnique({
      where: { id: sessionId },
      include: { user: { include: { pinCode: true } } }
    });

    if (!session || session.status === SessionStatus.REVOKED || session.expiresAt < new Date()) {
      throw new UnauthorizedException('Сессия недействительна');
    }

    if (!session.user.pinCode?.isEnabled) {
      throw new BadRequestException('PIN-код не включен');
    }

    const matches = await bcrypt.compare(pin, session.user.pinCode.hash);
    if (!matches) {
      throw new UnauthorizedException('Неверный PIN-код');
    }

    await this.prisma.session.update({
      where: { id: sessionId },
      data: {
        pinVerified: true,
        status: SessionStatus.ACTIVE,
        updatedAt: new Date()
      }
    });

    return {
      accessToken: await this.auth.issueAccessToken(session.user, session.id, true),
      pinVerified: true
    };
  }

  async lockExpiredPinSessions() {
    return this.session.lockExpiredPinSessions();
  }
}
