import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import * as crypto from 'crypto';
import * as QRCode from 'qrcode';
import {
  AccessBadgeType,
  AccessDirection,
  AccessResult,
  Prisma,
} from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { AuthenticatedUser } from '../../common/decorators/current-user.decorator';
import { StubPlateRecognitionProvider } from './providers/plate-recognition.provider';

interface IssueBadgeInput {
  userId: string;
  complexId: string;
  type?: AccessBadgeType;
  label?: string;
  validUntil?: Date;
  plateNumber?: string; // for LICENSE_PLATE type
}

interface VerifyAccessInput {
  complexId: string;
  code?: string;          // QR/RFID code
  plateImageBase64?: string; // base64 image for OCR
  direction?: AccessDirection;
  gate?: string;
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly ocr: StubPlateRecognitionProvider,
  ) {}

  /**
   * Issue an access badge. For QR: generates a secure random token and a
   * PNG data-URL of the QR. For LICENSE_PLATE: the plate is the code.
   */
  async issueBadge(user: AuthenticatedUser, dto: IssueBadgeInput) {
    if (!user.tenantId) throw new BadRequestException('Tenant required');
    await this.assertComplexAccess(user, dto.complexId);
    // Ensure target user exists and is a resident of this complex
    const resident = await this.prisma.resident.findFirst({
      where: { userId: dto.userId, lot: { complexId: dto.complexId }, endDate: null },
    });
    if (!resident && !user.isSuperAdmin) {
      throw new BadRequestException('Target user is not an active resident of the complex');
    }

    const type = dto.type ?? AccessBadgeType.QR;
    const code =
      type === AccessBadgeType.LICENSE_PLATE
        ? (dto.plateNumber ?? '').toUpperCase()
        : crypto.randomBytes(16).toString('base64url');
    if (!code) throw new BadRequestException('Missing code/plate');

    const badge = await this.prisma.accessBadge.create({
      data: {
        userId: dto.userId,
        complexId: dto.complexId,
        type,
        code,
        label: dto.label,
        validUntil: dto.validUntil,
      },
    });

    let qrDataUrl: string | undefined;
    if (type === AccessBadgeType.QR) {
      qrDataUrl = await QRCode.toDataURL(
        JSON.stringify({ badgeId: badge.id, code, complexId: dto.complexId }),
      );
    }
    return { badge, qrDataUrl };
  }

  async listBadges(user: AuthenticatedUser, complexId?: string) {
    if (complexId) await this.assertComplexAccess(user, complexId);
    const accessibleComplexIds = complexId ? [complexId] : await this.listAccessibleComplexIds(user);
    return this.prisma.accessBadge.findMany({
      where: {
        ...(!user.isSuperAdmin && { complexId: { in: accessibleComplexIds } }),
        ...(user.isSuperAdmin && complexId && { complexId }),
        ...(user.roles.some((r) => ['COPROPRIETAIRE', 'LOCATAIRE'].includes(r.code)) &&
          !user.roles.some((r) => ['SUPERADMIN', 'SYNDIC', 'GARDIEN'].includes(r.code)) && {
            userId: user.userId,
          }),
      },
      orderBy: { createdAt: 'desc' },
      include: { user: { select: { id: true, email: true, firstName: true } } },
    });
  }

  async revokeBadge(user: AuthenticatedUser, id: string) {
    const b = await this.prisma.accessBadge.findUnique({ where: { id } });
    if (!b) throw new NotFoundException();
    await this.assertComplexAccess(user, b.complexId);
    return this.prisma.accessBadge.update({
      where: { id },
      data: { revoked: true },
    });
  }

  /**
   * Verify an access attempt. Returns GRANTED/DENIED and writes an AccessLog.
   * Public-ish — called by gate/RFID hardware (protect via API key in real deployment).
   */
  async verify(dto: VerifyAccessInput) {
    let code = dto.code;
    let type: AccessBadgeType = AccessBadgeType.QR;
    let confidence: number | undefined;

    if (!code && dto.plateImageBase64) {
      const hits = await this.ocr.recognize(dto.plateImageBase64);
      const best = hits.sort((a, b) => b.confidence - a.confidence)[0];
      if (best && best.confidence >= 0.7) {
        code = best.plate;
        type = AccessBadgeType.LICENSE_PLATE;
        confidence = best.confidence;
      }
    }

    let result: AccessResult = AccessResult.DENIED;
    let userId: string | null = null;

    if (code) {
      const badge = await this.prisma.accessBadge.findFirst({
        where: {
          code,
          complexId: dto.complexId,
          revoked: false,
          OR: [{ validUntil: null }, { validUntil: { gte: new Date() } }],
        },
      });
      if (badge) {
        result = AccessResult.GRANTED;
        userId = badge.userId;
        type = badge.type;
      }
    }

    const log = await this.prisma.accessLog.create({
      data: {
        complexId: dto.complexId,
        userId,
        badgeCode: code,
        type,
        direction: dto.direction ?? AccessDirection.IN,
        result,
        gate: dto.gate,
        confidence,
        rawPayload: confidence != null ? ({ ocrConfidence: confidence } as Prisma.InputJsonValue) : undefined,
      },
    });
    return { granted: result === AccessResult.GRANTED, logId: log.id, userId, type };
  }

  async listLogs(user: AuthenticatedUser, complexId: string, limit = 100) {
    await this.assertComplexAccess(user, complexId);
    return this.prisma.accessLog.findMany({
      where: { complexId },
      orderBy: { createdAt: 'desc' },
      take: Math.min(Math.max(limit, 1), 500),
      include: { user: { select: { id: true, email: true, firstName: true } } },
    });
  }

  private async assertComplexAccess(user: AuthenticatedUser, complexId: string) {
    if (user.isSuperAdmin) return;
    const complex = await this.prisma.complex.findFirst({
      where: { id: complexId, tenantId: user.tenantId ?? undefined },
      select: { id: true },
    });
    if (!complex) throw new NotFoundException('Complex not found');
  }

  private async listAccessibleComplexIds(user: AuthenticatedUser) {
    if (user.isSuperAdmin) return undefined;
    const complexes = await this.prisma.complex.findMany({
      where: { tenantId: user.tenantId ?? undefined },
      select: { id: true },
    });
    return complexes.map((complex) => complex.id);
  }
}
