import {
  Injectable,
  NotFoundException,
  BadRequestException,
  ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { BookingRecordStatus } from '@prisma/client';
import { AvailabilityService } from '../core/availability.service';
import { BookingEventsService } from '../core/booking-events.service';
import { ClientMembershipsService } from './client-memberships.service';
import {
  CreateBookingRecordDto,
  UpdateBookingRecordDto,
  UpdateRecordStatusDto,
  FindRecordsQueryDto,
} from '../dto/create-record.dto';

const RECORD_INCLUDE = {
  service: { select: { id: true, name: true, duration: true, type: true, color: true } },
  staff: { select: { id: true, firstName: true, lastName: true, avatarUrl: true } },
  groupEvent: { select: { id: true, title: true, capacity: true, bookedCount: true } },
  client: { select: { id: true, firstName: true, lastName: true, phone: true } },
  payments: { select: { id: true, amount: true, method: true, status: true } },
};

/** Valid status transitions */
const STATUS_TRANSITIONS: Record<BookingRecordStatus, BookingRecordStatus[]> = {
  PENDING: ['CONFIRMED', 'CANCELLED'],
  CONFIRMED: ['IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'NO_SHOW'],
  IN_PROGRESS: ['COMPLETED', 'CANCELLED'],
  COMPLETED: [],
  CANCELLED: [],
  NO_SHOW: [],
};

@Injectable()
export class BookingRecordsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly availability: AvailabilityService,
    private readonly events: BookingEventsService,
    private readonly clientMemberships: ClientMembershipsService,
  ) {}

  // ─── List ───────────────────────────────────────────────────

  async findAll(clubId: string, query: FindRecordsQueryDto, roleFilter: Record<string, any>) {
    const limit = Math.min(query.limit ?? 50, 100);

    const where: Record<string, any> = {
      clubId,
      ...roleFilter,
      ...(query.status ? { status: query.status } : {}),
      ...(query.staffId ? { staffId: query.staffId } : {}),
      ...(query.serviceId ? { serviceId: query.serviceId } : {}),
    };

    if (query.from || query.to) {
      where.startAt = {};
      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      if (query.from) where.startAt.gte = new Date(query.from);
      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      if (query.to) where.startAt.lte = new Date(query.to + 'T23:59:59');
    }

    if (query.search) {
      where.OR = [
        { clientName: { contains: query.search, mode: 'insensitive' } },
        { clientPhone: { contains: query.search } },
      ];
    }

    const items = await this.prisma.bookingRecord.findMany({
      where,
      include: RECORD_INCLUDE,
      take: limit + 1,
      ...(query.cursor ? { cursor: { id: query.cursor }, skip: 1 } : {}),
      orderBy: { startAt: 'desc' },
    });

    const hasMore = items.length > limit;
    if (hasMore) items.pop();

    return {
      items,
      nextCursor: hasMore ? items[items.length - 1]?.id : null,
      hasMore,
    };
  }

  // ─── Get by ID ──────────────────────────────────────────────

  async findById(id: string) {
    const record = await this.prisma.bookingRecord.findUnique({
      where: { id },
      include: RECORD_INCLUDE,
    });
    if (!record) throw new NotFoundException(`Record ${id} not found`);
    return record;
  }

  // ─── Create ─────────────────────────────────────────────────

  async create(clubId: string, dto: CreateBookingRecordDto, userId?: string) {
    const service = await this.prisma.bookingService.findUniqueOrThrow({
      where: { id: dto.serviceId },
    });

    const startAt = new Date(dto.startAt);
    const endAt = dto.endAt
      ? new Date(dto.endAt)
      : new Date(startAt.getTime() + service.duration * 60 * 1000);

    // For individual bookings, check availability
    if (!dto.groupEventId) {
      const available = await this.availability.isSlotAvailable({
        clubId,
        staffId: dto.staffId,
        startAt,
        endAt,
      });
      if (!available) {
        throw new ConflictException('Time slot is not available');
      }
    }

    // For group bookings, check capacity and membership requirement
    if (dto.groupEventId) {
      const event = await this.prisma.bookingGroupEvent.findUniqueOrThrow({
        where: { id: dto.groupEventId },
      });
      const places = dto.places ?? 1;
      if (event.bookedCount + places > event.capacity) {
        throw new ConflictException(
          `Not enough capacity: ${event.capacity - event.bookedCount} places remaining`,
        );
      }

      // Check if group event requires membership
      const meta = event.metadata as Record<string, any> | null;
      if (meta?.requireMembership && !dto.membershipId) {
        throw new BadRequestException('This group event requires a valid membership to book');
      }
    }

    // Validate membership if provided
    if (dto.membershipId) {
      const valid = await this.clientMemberships.isValidForService(dto.membershipId, dto.serviceId);
      if (!valid) {
        throw new BadRequestException(
          'Membership is not valid for this service (expired, frozen, depleted, or wrong service)',
        );
      }
    }

    // Generate sequential number
    const lastRecord = await this.prisma.bookingRecord.findFirst({
      where: { clubId },
      orderBy: { number: 'desc' },
      select: { number: true },
    });
    const number = (lastRecord?.number ?? 0) + 1;

    const record = await this.prisma.bookingRecord.create({
      data: {
        clubId,
        number,
        clientName: dto.clientName,
        clientPhone: dto.clientPhone,
        clientEmail: dto.clientEmail,
        clientId: dto.clientId,
        serviceId: dto.serviceId,
        staffId: dto.staffId,
        groupEventId: dto.groupEventId,
        startAt,
        endAt,
        status: 'PENDING',
        source: dto.source ?? 'ADMIN',
        places: dto.places ?? 1,
        price: service.price,
        membershipId: dto.membershipId,
        notes: dto.notes,
        metadata: dto.metadata ?? undefined,
        createdById: userId,
      },
      include: RECORD_INCLUDE,
    });

    // Update bookedCount for group events
    if (dto.groupEventId) {
      await this.prisma.bookingGroupEvent.update({
        where: { id: dto.groupEventId },
        data: { bookedCount: { increment: dto.places ?? 1 } },
      });
    }

    await this.events.emit('booking.record.created', {
      clubId,
      data: { record, source: dto.source ?? 'ADMIN' },
      userId,
    });

    return record;
  }

  // ─── Update ─────────────────────────────────────────────────

  async update(id: string, dto: UpdateBookingRecordDto) {
    const existing = await this.findById(id);

    if (['COMPLETED', 'CANCELLED', 'NO_SHOW'].includes(existing.status)) {
      throw new BadRequestException('Cannot modify a completed/cancelled record');
    }

    // If time changed, re-check availability
    if (dto.startAt || dto.endAt || dto.staffId) {
      const startAt = dto.startAt ? new Date(dto.startAt) : existing.startAt;
      const endAt = dto.endAt ? new Date(dto.endAt) : existing.endAt;
      const staffId = dto.staffId ?? existing.staffId;

      if (!existing.groupEventId) {
        const available = await this.availability.isSlotAvailable({
          clubId: existing.clubId,
          staffId,
          startAt,
          endAt,
          excludeRecordId: id,
        });
        if (!available) {
          throw new ConflictException('New time slot is not available');
        }
      }
    }

    const record = await this.prisma.bookingRecord.update({
      where: { id },
      data: {
        ...(dto.clientName !== undefined ? { clientName: dto.clientName } : {}),
        ...(dto.clientPhone !== undefined ? { clientPhone: dto.clientPhone } : {}),
        ...(dto.clientEmail !== undefined ? { clientEmail: dto.clientEmail } : {}),
        ...(dto.staffId !== undefined ? { staffId: dto.staffId } : {}),
        ...(dto.startAt !== undefined ? { startAt: new Date(dto.startAt) } : {}),
        ...(dto.endAt !== undefined ? { endAt: new Date(dto.endAt) } : {}),
        ...(dto.notes !== undefined ? { notes: dto.notes } : {}),
      },
      include: RECORD_INCLUDE,
    });

    await this.events.emit('booking.record.updated', {
      clubId: record.clubId,
      data: { record, changes: dto },
    });

    return record;
  }

  // ─── Status Transition ──────────────────────────────────────

  async updateStatus(id: string, dto: UpdateRecordStatusDto, userId?: string) {
    const existing = await this.findById(id);

    const allowed = STATUS_TRANSITIONS[existing.status];
    if (!allowed.includes(dto.status)) {
      throw new BadRequestException(
        `Cannot transition from ${existing.status} to ${dto.status}. Allowed: ${allowed.join(', ') || 'none'}`,
      );
    }

    const data: Record<string, any> = { status: dto.status };

    switch (dto.status) {
      case 'CONFIRMED':
        data.confirmedAt = new Date();
        break;
      case 'COMPLETED':
        data.completedAt = new Date();
        break;
      case 'CANCELLED':
        data.cancelledAt = new Date();
        data.cancelReason = dto.cancelReason;
        // Free up group event places
        if (existing.groupEventId) {
          await this.prisma.bookingGroupEvent.update({
            where: { id: existing.groupEventId },
            data: { bookedCount: { decrement: existing.places } },
          });
        }
        break;
      case 'NO_SHOW':
        break;
    }

    const record = await this.prisma.bookingRecord.update({
      where: { id },
      data,
      include: RECORD_INCLUDE,
    });

    // Emit status-specific events
    const eventMap: Record<string, any> = {
      CONFIRMED: 'booking.record.confirmed',
      COMPLETED: 'booking.record.completed',
      CANCELLED: 'booking.record.cancelled',
      NO_SHOW: 'booking.record.noshow',
    };

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const eventType = eventMap[dto.status];
    if (eventType) {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      await this.events.emit(eventType, {
        clubId: record.clubId,
        data: { record, previousStatus: existing.status },
        userId,
      });
    }

    return record;
  }

  // ─── Delete ─────────────────────────────────────────────────

  async delete(id: string) {
    const record = await this.findById(id);

    if (record.groupEventId && !['CANCELLED', 'NO_SHOW'].includes(record.status)) {
      await this.prisma.bookingGroupEvent.update({
        where: { id: record.groupEventId },
        data: { bookedCount: { decrement: record.places } },
      });
    }

    await this.prisma.bookingRecord.delete({ where: { id } });

    await this.events.emit('booking.record.cancelled', {
      clubId: record.clubId,
      data: { record, reason: 'deleted' },
    });
  }

  // ─── Today's schedule for a trainer ─────────────────────────

  async getTrainerSchedule(clubId: string, staffId: string, date: Date) {
    const dayStart = new Date(date);
    dayStart.setHours(0, 0, 0, 0);
    const dayEnd = new Date(date);
    dayEnd.setHours(23, 59, 59, 999);

    return this.prisma.bookingRecord.findMany({
      where: {
        clubId,
        staffId,
        startAt: { gte: dayStart, lte: dayEnd },
        status: { notIn: ['CANCELLED'] },
      },
      include: RECORD_INCLUDE,
      orderBy: { startAt: 'asc' },
    });
  }
}
