import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { NotificationChannel, Prisma, VisitorPassStatus, VisitSessionStatus } from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import { resolveEstateScope } from '../common/estate-scope';
import { assertActiveGuardForEstate } from '../common/guard-access';
import { RequestUser } from '../common/request-user';
import { PrismaService } from '../database/prisma.service';
import { CheckoutDto, ManualCheckinDto, ScanCheckinDto } from './dto/checkin.dto';

type PassLookup = { estateId: string; qrToken?: string; visitorCode?: string };

@Injectable()
export class CheckinService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService
  ) {}

  async checkinByQr(dto: ScanCheckinDto, user: RequestUser) {
    const estateId = resolveEstateScope(user, dto.estateId);
    const guard = await assertActiveGuardForEstate(this.prisma, dto.guardId, estateId, user, dto.deviceId);
    return this.checkin(
      { estateId, qrToken: dto.qrToken },
      dto.guardId,
      dto.entryGate,
      dto.gateId,
      dto.offlineEventId,
      dto.deviceId ?? guard.deviceId ?? undefined,
      dto.scanLocation,
      user
    );
  }

  async checkinByCode(dto: ManualCheckinDto, user: RequestUser) {
    const estateId = resolveEstateScope(user, dto.estateId);
    const guard = await assertActiveGuardForEstate(this.prisma, dto.guardId, estateId, user, dto.deviceId);
    return this.checkin(
      { estateId, visitorCode: dto.visitorCode },
      dto.guardId,
      dto.entryGate,
      dto.gateId,
      dto.offlineEventId,
      dto.deviceId ?? guard.deviceId ?? undefined,
      dto.scanLocation,
      user
    );
  }

  async checkout(dto: CheckoutDto, user: RequestUser) {
    const session = await this.prisma.visitSession.findUnique({
      where: { id: dto.visitSessionId },
      include: { visitorPass: true }
    });
    if (!session) {
      throw new NotFoundException('Visit session not found');
    }
    const estateId = resolveEstateScope(user, session.estateId);
    const guard = await assertActiveGuardForEstate(this.prisma, dto.guardId, estateId, user, dto.deviceId);
    if (session.status === VisitSessionStatus.CHECKED_OUT) {
      throw new BadRequestException('Visit session is already checked out');
    }

    const updated = await this.prisma.visitSession.update({
      where: { id: dto.visitSessionId },
      data: {
        guardId: dto.guardId,
        exitGate: dto.exitGate,
        gateId: dto.gateId ?? session.gateId,
        checkoutTime: new Date(),
        status: VisitSessionStatus.CHECKED_OUT
      },
      include: { visitorPass: { include: { visitor: true, hostResident: true } } }
    });

    await this.createResidentVisitNotification(updated, 'Visitor checked out', 'Your visitor has checked out.');

    await this.audit.record({
      estateId: updated.estateId,
      actorType: 'GUARD',
      actorId: dto.guardId,
      action: 'VISITOR_CHECKED_OUT',
      metadata: {
        visitSessionId: updated.id,
        visitorPassId: updated.visitorPassId,
        deviceId: dto.deviceId ?? guard.deviceId ?? null
      }
    });
    return updated;
  }

  private async checkin(
    lookup: PassLookup,
    guardId: string,
    entryGate: string | undefined,
    gateId: string | undefined,
    offlineEventId: string | undefined,
    deviceId: string | undefined,
    scanLocation: Record<string, unknown> | undefined,
    user: RequestUser
  ) {
    const now = new Date();
    const where: Prisma.VisitorPassWhereInput = {
      estateId: lookup.estateId,
      qrToken: lookup.qrToken,
      visitorCode: lookup.visitorCode
    };

    const pass = await this.prisma.visitorPass.findFirst({
      where,
      include: { visitor: true, hostResident: true }
    });
    if (!pass) {
      throw new NotFoundException('Visitor pass not found');
    }
    if (pass.status !== VisitorPassStatus.ACTIVE) {
      throw new BadRequestException(`Visitor pass is ${pass.status.toLowerCase()}`);
    }
    if (pass.validFrom > now || pass.validUntil < now) {
      throw new BadRequestException('Visitor pass is outside its validity window');
    }
    if (pass.entryCount >= pass.maxEntries) {
      throw new BadRequestException('Visitor pass has no remaining entries');
    }

    const resolvedGateId = await this.resolveGateId(pass.estateId, gateId, entryGate);

    const session = await this.prisma.$transaction(async (tx) => {
      const updateResult = await tx.visitorPass.updateMany({
        where: {
          id: pass.id,
          status: VisitorPassStatus.ACTIVE,
          entryCount: { lt: pass.maxEntries }
        },
        data: {
          entryCount: { increment: 1 }
        }
      });

      if (updateResult.count !== 1) {
        throw new BadRequestException('Visitor pass has no remaining entries');
      }

      const updatedPass = await tx.visitorPass.findUniqueOrThrow({
        where: { id: pass.id },
        select: { entryCount: true, maxEntries: true }
      });

      if (updatedPass.entryCount >= updatedPass.maxEntries) {
        await tx.visitorPass.update({
          where: { id: pass.id },
          data: { status: VisitorPassStatus.USED }
        });
      }

      return tx.visitSession.create({
        data: {
          estateId: pass.estateId,
          visitorPassId: pass.id,
          guardId,
          deviceId,
          gateId: resolvedGateId,
          visitorPhone: pass.visitor.phone,
          hostResidentId: pass.hostResidentId,
          entryGate,
          offlineEventId,
          scanLocation: this.toJson(scanLocation),
          metadata: {
            visitorId: pass.visitorId,
            residentId: pass.hostResidentId,
            visitorPhone: pass.visitor.phone,
            gateLabel: entryGate ?? null
          }
        },
        include: { visitorPass: { include: { visitor: true, hostResident: true } } }
      });
    });

    await this.createResidentVisitNotification(session, 'Visitor checked in', 'Your visitor has checked in at the gate.');

    await this.audit.record({
      estateId: pass.estateId,
      actorType: 'GUARD',
      actorId: guardId,
      action: 'VISITOR_CHECKED_IN',
      metadata: {
        visitSessionId: session.id,
        visitorPassId: pass.id,
        deviceId: deviceId ?? null,
        checkedInBy: user.id
      }
    });
    return session;
  }

  private async resolveGateId(estateId: string, gateId?: string, gateName?: string) {
    if (gateId) {
      const gate = await this.prisma.estateGate.findFirst({
        where: { id: gateId, estateId, isActive: true },
        select: { id: true }
      });
      if (!gate) {
        throw new BadRequestException('Gate is not active in this estate');
      }
      return gate.id;
    }

    if (!gateName) {
      const mainGate = await this.prisma.estateGate.findFirst({
        where: { estateId, isActive: true },
        orderBy: { createdAt: 'asc' },
        select: { id: true }
      });
      return mainGate?.id;
    }

    const gate = await this.prisma.estateGate.findFirst({
      where: { estateId, name: { equals: gateName, mode: 'insensitive' }, isActive: true },
      select: { id: true }
    });
    return gate?.id;
  }

  private async createResidentVisitNotification(
    session: {
      estateId: string;
      id: string;
      visitorPass: { visitor: { name: string; phone: string | null }; hostResident: { userId: string; name: string } };
      status: VisitSessionStatus;
    },
    title: string,
    body: string
  ) {
    await this.prisma.appNotification.create({
      data: {
        estateId: session.estateId,
        userId: session.visitorPass.hostResident.userId,
        channel: NotificationChannel.IN_APP,
        title,
        body: `${body} ${session.visitorPass.visitor.name}`,
        data: {
          visitSessionId: session.id,
          visitorName: session.visitorPass.visitor.name,
          visitorPhone: session.visitorPass.visitor.phone,
          status: session.status
        }
      }
    });
  }

  private toJson(value: unknown) {
    if (value === undefined) {
      return undefined;
    }
    return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
  }
}
