import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateWorkScheduleDto } from './dto/create-work-schedule.dto';
import { UpdateWorkScheduleDto } from './dto/update-work-schedule.dto';
import { FindWorkScheduleDto } from './dto/find-work-schedule.dto';
import { Prisma } from '@prisma/client';

/** Standard includes for work schedule queries. */
const WORK_SCHEDULE_INCLUDE = {
  trainer: {
    select: { id: true, firstName: true, lastName: true },
  },
};

@Injectable()
export class WorkScheduleService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  /**
   * Find all work schedule entries filtered by club, trainer, and date range.
   */
  async findAll(clubId: string, params: FindWorkScheduleDto) {
    const { trainerId, dateFrom, dateTo } = params;

    const where: Prisma.WorkScheduleWhereInput = {
      clubId,
      ...(trainerId ? { trainerId } : {}),
      ...this.buildDateFilter(dateFrom, dateTo),
    };

    return this.prisma.workSchedule.findMany({
      where,
      orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
      include: WORK_SCHEDULE_INCLUDE,
    });
  }

  /**
   * Find a single work schedule entry by ID.
   */
  async findById(clubId: string, id: string) {
    const entry = await this.prisma.workSchedule.findFirst({
      where: { id, clubId },
      include: WORK_SCHEDULE_INCLUDE,
    });

    if (!entry) {
      throw new NotFoundException(
        `Work schedule entry with id ${id} not found`,
      );
    }

    return entry;
  }

  /**
   * Find a work schedule entry for a specific trainer on a specific date.
   */
  async findByTrainerAndDate(
    clubId: string,
    trainerId: string,
    date: string,
  ) {
    const dayStart = new Date(`${date}T00:00:00.000Z`);
    const dayEnd = new Date(`${date}T23:59:59.999Z`);

    return this.prisma.workSchedule.findMany({
      where: {
        clubId,
        trainerId,
        date: { gte: dayStart, lte: dayEnd },
      },
      orderBy: { startTime: 'asc' },
      include: WORK_SCHEDULE_INCLUDE,
    });
  }

  /**
   * Create a single work schedule entry.
   * Validates that startTime < endTime.
   */
  async create(clubId: string, dto: CreateWorkScheduleDto) {
    this.validateTimeRange(dto.startTime, dto.endTime);

    const schedule = await this.prisma.workSchedule.create({
      data: {
        clubId,
        trainerId: dto.trainerId!,
        date: new Date(dto.date),
        startTime: dto.startTime,
        endTime: dto.endTime,
        isAvailable: dto.isAvailable ?? true,
      },
      include: WORK_SCHEDULE_INCLUDE,
    });

    this.eventEmitter.emit('schedule.updated', { clubId, data: schedule });

    return schedule;
  }

  /**
   * Update a work schedule entry.
   * Validates time range if both start and end are updated.
   */
  async update(clubId: string, id: string, dto: UpdateWorkScheduleDto) {
    const existing = await this.ensureExists(clubId, id);

    const startTime = dto.startTime ?? existing.startTime;
    const endTime = dto.endTime ?? existing.endTime;
    this.validateTimeRange(startTime, endTime);

    const updated = await this.prisma.workSchedule.update({
      where: { id },
      data: {
        ...(dto.startTime !== undefined ? { startTime: dto.startTime } : {}),
        ...(dto.endTime !== undefined ? { endTime: dto.endTime } : {}),
        ...(dto.isAvailable !== undefined
          ? { isAvailable: dto.isAvailable }
          : {}),
      },
      include: WORK_SCHEDULE_INCLUDE,
    });

    this.eventEmitter.emit('schedule.updated', { clubId, data: updated });

    return updated;
  }

  /**
   * Delete a work schedule entry.
   */
  async remove(clubId: string, id: string) {
    await this.ensureExists(clubId, id);

    return this.prisma.workSchedule.delete({
      where: { id },
      include: WORK_SCHEDULE_INCLUDE,
    });
  }

  /**
   * Bulk-create multiple work schedule entries (e.g. for setting a weekly schedule).
   * Uses a transaction to ensure atomicity.
   */
  async bulkCreate(clubId: string, entries: CreateWorkScheduleDto[]) {
    for (const entry of entries) {
      this.validateTimeRange(entry.startTime, entry.endTime);
    }

    return this.prisma.$transaction(
      entries.map((entry) =>
        this.prisma.workSchedule.create({
          data: {
            clubId,
            trainerId: entry.trainerId!,
            date: new Date(entry.date),
            startTime: entry.startTime,
            endTime: entry.endTime,
            isAvailable: entry.isAvailable ?? true,
          },
          include: WORK_SCHEDULE_INCLUDE,
        }),
      ),
    );
  }

  // ---------------------------------------------------------------------------
  // Private helpers
  // ---------------------------------------------------------------------------

  /**
   * Build a date range filter for the `date` field.
   */
  private buildDateFilter(
    dateFrom?: string,
    dateTo?: string,
  ): Prisma.WorkScheduleWhereInput {
    if (!dateFrom && !dateTo) return {};

    const date: Prisma.DateTimeFilter = {};
    if (dateFrom) date.gte = new Date(dateFrom);
    if (dateTo) date.lte = new Date(dateTo);

    return { date };
  }

  /**
   * Validate that startTime is strictly before endTime.
   */
  private validateTimeRange(startTime: string, endTime: string): void {
    if (startTime >= endTime) {
      throw new BadRequestException(
        `startTime (${startTime}) must be before endTime (${endTime})`,
      );
    }
  }

  /**
   * Ensure a work schedule entry exists within the club, or throw.
   */
  private async ensureExists(clubId: string, id: string) {
    const entry = await this.prisma.workSchedule.findFirst({
      where: { id, clubId },
    });

    if (!entry) {
      throw new NotFoundException(
        `Work schedule entry with id ${id} not found`,
      );
    }

    return entry;
  }
}
