import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DistanceApprovalStatus, ViharStatus } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.service';
import { RoutesService } from '../routes/routes.service';
import type { CheckInInput, CheckOutInput } from '@vihar/shared';

@Injectable()
export class AllocationsService {
  private readonly logger = new Logger(AllocationsService.name);
  private readonly toleranceKm: number;

  constructor(
    private readonly prisma: PrismaService,
    private readonly routes: RoutesService,
    config: ConfigService,
  ) {
    this.toleranceKm = parseFloat(config.get<string>('DISTANCE_TOLERANCE_KM') ?? '1.0');
  }

  /**
   * Allocate one or more volunteers to a vihar. Captain only.
   * Idempotent on (vihar, user) unique-active pairs - re-allocating someone
   * already active is a no-op.
   */
  async allocate(cityId: number, viharId: number, allocatedBy: number, userIds: number[]) {
    const vihar = await this.prisma.vihar.findUnique({ where: { viharId } });
    if (!vihar || vihar.cityId !== cityId) throw new NotFoundException('Vihar not found');
    if (vihar.status === ViharStatus.cancelled || vihar.status === ViharStatus.completed) {
      throw new BadRequestException(`Cannot allocate to a ${vihar.status} vihar`);
    }

    // Verify all users are in the same city
    const users = await this.prisma.user.findMany({
      where: { userId: { in: userIds }, cityId, isActive: true, isVolunteer: true },
      select: { userId: true },
    });
    if (users.length !== userIds.length) {
      throw new BadRequestException('One or more user IDs are invalid for this city');
    }

    return this.prisma.$transaction(async (tx) => {
      const created: any[] = [];
      for (const userId of userIds) {
        const existing = await tx.viharVolunteer.findFirst({
          where: { viharId, userId, isActive: true },
        });
        if (existing) {
          created.push(existing);
          continue;
        }
        const row = await tx.viharVolunteer.create({
          data: { viharId, userId, allocatedBy },
        });
        created.push(row);
      }
      return created;
    });
  }

  async deallocate(cityId: number, allocationId: number, _actorId: number) {
    const a = await this.prisma.viharVolunteer.findUnique({
      where: { allocationId },
      include: { vihar: true },
    });
    if (!a || a.vihar.cityId !== cityId) throw new NotFoundException('Allocation not found');
    if (!a.isActive) throw new BadRequestException('Already deallocated');
    return this.prisma.viharVolunteer.update({
      where: { allocationId },
      data: { isActive: false },
    });
  }

  async checkIn(cityId: number, allocationId: number, actorId: number, input: CheckInInput) {
    const a = await this.findOwnAllocation(cityId, allocationId, actorId);
    if (a.checkInAt) throw new BadRequestException('Already checked in');

    const now = new Date();
    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.viharVolunteer.update({
        where: { allocationId },
        data: {
          checkInAt: now,
          checkInLat: input.latitude ?? null,
          checkInLng: input.longitude ?? null,
          checkInAccuracyM: input.accuracyMeters ?? null,
          checkInWithoutGps: input.withoutGps,
        },
      });
      // Move parent vihar to in_progress if currently planned
      await tx.vihar.updateMany({
        where: { viharId: a.viharId, status: ViharStatus.planned },
        data: { status: ViharStatus.in_progress, actualStartAt: now },
      });
      return updated;
    });
  }

  async checkOut(
    cityId: number,
    allocationId: number,
    actorId: number,
    input: CheckOutInput,
    onBehalfByCaptain: boolean,
  ) {
    const a = await this.prisma.viharVolunteer.findUnique({
      where: { allocationId },
      include: { vihar: true, user: true },
    });
    if (!a || a.vihar.cityId !== cityId) throw new NotFoundException('Allocation not found');
    if (!a.isActive) throw new BadRequestException('Allocation is inactive');
    if (a.checkOutAt) throw new BadRequestException('Already checked out');

    if (!onBehalfByCaptain && a.userId !== actorId) {
      throw new ForbiddenException('You can only check out your own allocation');
    }

    // Determine if distance is within tolerance
    let plannedKm: number | null = null;
    try {
      const route = await this.routes.getWalkingByLocationIds(
        cityId,
        a.vihar.departureLocationId,
        a.vihar.arrivalLocationId,
      );
      plannedKm = route.distanceMeters / 1000;
    } catch {
      // ignore - if routes unavailable, accept volunteer's number as-is
    }

    const approvalStatus: DistanceApprovalStatus =
      plannedKm !== null && Math.abs(input.distanceKm - plannedKm) > this.toleranceKm
        ? DistanceApprovalStatus.pending_captain
        : DistanceApprovalStatus.auto_accepted;

    const now = new Date();

    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.viharVolunteer.update({
        where: { allocationId },
        data: {
          checkOutAt: now,
          checkOutLat: input.latitude ?? null,
          checkOutLng: input.longitude ?? null,
          checkOutAccuracyM: input.accuracyMeters ?? null,
          enteredStartTime: input.startTime,
          volunteerDistanceKm: input.distanceKm,
          distanceApprovalStatus: approvalStatus,
          enteredByCaptain: onBehalfByCaptain,
          notes: input.notes ?? null,
        },
      });

      // If this is the last active allocation without checkout, close the vihar
      const stillOpen = await tx.viharVolunteer.count({
        where: { viharId: a.viharId, isActive: true, checkOutAt: null },
      });
      if (stillOpen === 0) {
        await tx.vihar.update({
          where: { viharId: a.viharId },
          data: {
            status: ViharStatus.completed,
            actualEndAt: now,
            actualDistanceKm: input.distanceKm,
            closedByUserId: actorId,
            closedByCaptain: onBehalfByCaptain,
            distanceWasOverridden: approvalStatus === DistanceApprovalStatus.pending_captain,
          },
        });
      }
      return updated;
    });
  }

  async approveDistance(
    cityId: number,
    allocationId: number,
    captainId: number,
    input: { approvedKm: number; decision: 'approved' | 'rejected' },
  ) {
    const a = await this.prisma.viharVolunteer.findUnique({
      where: { allocationId },
      include: { vihar: true },
    });
    if (!a || a.vihar.cityId !== cityId) throw new NotFoundException('Allocation not found');
    if (a.distanceApprovalStatus !== DistanceApprovalStatus.pending_captain) {
      throw new BadRequestException('Allocation is not pending distance approval');
    }
    return this.prisma.viharVolunteer.update({
      where: { allocationId },
      data: {
        distanceApprovalStatus:
          input.decision === 'approved'
            ? DistanceApprovalStatus.approved
            : DistanceApprovalStatus.rejected,
        volunteerDistanceKm: input.approvedKm,
        distanceApprovedBy: captainId,
        distanceApprovedAt: new Date(),
      },
    });
  }

  private async findOwnAllocation(cityId: number, allocationId: number, userId: number) {
    const a = await this.prisma.viharVolunteer.findUnique({
      where: { allocationId },
      include: { vihar: true },
    });
    if (!a || a.vihar.cityId !== cityId) throw new NotFoundException('Allocation not found');
    if (a.userId !== userId) throw new ForbiddenException('Not your allocation');
    if (!a.isActive) throw new BadRequestException('Allocation is inactive');
    return a;
  }
}
