import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateTrainingDto } from './dto/create-training.dto';
import { UpdateTrainingDto } from './dto/update-training.dto';
import { RescheduleTrainingDto } from './dto/reschedule-training.dto';
import { CancelTrainingDto } from './dto/cancel-training.dto';
import { FindTrainingsDto } from './dto/find-trainings.dto';
import { Prisma } from '@prisma/client';

export interface PaginatedResult<T> {
  data: T[];
  nextCursor: string | null;
  hasMore: boolean;
}

export interface TimeSlot {
  startTime: string;
  endTime: string;
}

/** Statuses that allow modifications (cancel, reschedule, update). */
const MODIFIABLE_STATUSES = ['PLANNED', 'CONFIRMED'];

/** Standard includes for training queries. */
const TRAINING_INCLUDE = {
  trainer: {
    select: { id: true, firstName: true, lastName: true },
  },
  client: {
    select: { id: true, firstName: true, lastName: true, phone: true },
  },
  service: {
    select: { id: true, name: true, duration: true },
  },
  room: {
    select: { id: true, name: true },
  },
};

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

  /**
   * Find all trainings with cursor-based pagination and filters.
   * RLS ensures club isolation via TenantMiddleware.
   */
  async findAll(
    clubId: string,
    params: FindTrainingsDto,
  ): Promise<PaginatedResult<any>> {
    const {
      cursor,
      limit = 20,
      trainerId,
      clientId,
      dateFrom,
      dateTo,
      status,
    } = params;

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

    const trainings = await this.prisma.training.findMany({
      where,
      take: limit + 1,
      ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
      orderBy: { scheduledAt: 'asc' },
      include: TRAINING_INCLUDE,
    });

    const hasMore = trainings.length > limit;
    const trimmed = hasMore ? trainings.slice(0, -1) : trainings;
    const data = this.transformTrainings(trimmed);

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

  /**
   * Find a single training by ID with all relations.
   */
  async findById(clubId: string, id: string) {
    const training = await this.prisma.training.findFirst({
      where: { id, clubId },
      include: {
        ...TRAINING_INCLUDE,
        repeatParent: {
          select: { id: true, scheduledAt: true },
        },
        repeatChildren: {
          select: { id: true, scheduledAt: true, status: true },
          orderBy: { scheduledAt: 'asc' },
        },
      },
    });

    if (!training) {
      throw new NotFoundException(`Training with id ${id} not found`);
    }

    return this.transformTraining(training);
  }

  /**
   * Create a training. If isRepeating + repeatCount, create N copies
   * with +7 days each using a $transaction.
   */
  async create(clubId: string, trainerId: string, dto: CreateTrainingDto) {
    const scheduledAt = new Date(dto.scheduledAt);
    const duration = dto.duration ?? 60;

    if (dto.isRepeating && dto.repeatCount && dto.repeatCount > 1) {
      const result = await this.createRepeatingTrainings(
        clubId,
        trainerId,
        dto,
        scheduledAt,
        duration,
      );
      this.eventEmitter.emit('training.created', { clubId, data: result });
      return result;
    }

    const training = await this.prisma.training.create({
      data: {
        clubId,
        trainerId,
        clientId: dto.clientId,
        serviceId: dto.serviceId,
        roomId: dto.roomId,
        scheduledAt,
        duration,
        isRepeating: false,
      },
      include: TRAINING_INCLUDE,
    });

    this.eventEmitter.emit('training.created', { clubId, data: training });

    return this.transformTraining(training);
  }

  /**
   * Update a training (only PLANNED status allowed).
   */
  async update(clubId: string, id: string, dto: UpdateTrainingDto) {
    const training = await this.ensureTrainingExists(clubId, id);

    if (training.status !== 'PLANNED') {
      throw new BadRequestException(
        'Only trainings with PLANNED status can be updated',
      );
    }

    const updated = await this.prisma.training.update({
      where: { id },
      data: {
        ...(dto.serviceId !== undefined ? { serviceId: dto.serviceId } : {}),
        ...(dto.roomId !== undefined ? { roomId: dto.roomId } : {}),
        ...(dto.scheduledAt !== undefined
          ? { scheduledAt: new Date(dto.scheduledAt) }
          : {}),
        ...(dto.duration !== undefined ? { duration: dto.duration } : {}),
        ...(dto.isRepeating !== undefined
          ? { isRepeating: dto.isRepeating }
          : {}),
      },
      include: TRAINING_INCLUDE,
    });

    return this.transformTraining(updated);
  }

  /**
   * Cancel a training with a reason. Only PLANNED or CONFIRMED.
   */
  async cancel(clubId: string, id: string, dto: CancelTrainingDto) {
    const training = await this.ensureTrainingExists(clubId, id);

    if (!MODIFIABLE_STATUSES.includes(training.status)) {
      throw new BadRequestException(
        'Only PLANNED or CONFIRMED trainings can be cancelled',
      );
    }

    const cancelled = await this.prisma.training.update({
      where: { id },
      data: {
        status: 'CANCELLED',
        cancelReason: dto.cancelReason,
        cancelComment: dto.cancelComment,
      },
      include: TRAINING_INCLUDE,
    });

    this.eventEmitter.emit('training.cancelled', { clubId, data: cancelled });

    return this.transformTraining(cancelled);
  }

  /**
   * Reschedule a training. Only PLANNED or CONFIRMED.
   * Stores previous scheduledAt in cancelComment for audit.
   */
  async reschedule(clubId: string, id: string, dto: RescheduleTrainingDto) {
    const training = await this.ensureTrainingExists(clubId, id);

    if (!MODIFIABLE_STATUSES.includes(training.status)) {
      throw new BadRequestException(
        'Only PLANNED or CONFIRMED trainings can be rescheduled',
      );
    }

    const previousTime = training.scheduledAt.toISOString();
    const rescheduleNote = `Rescheduled from ${previousTime}`;

    const rescheduled = await this.prisma.training.update({
      where: { id },
      data: {
        scheduledAt: new Date(dto.scheduledAt),
        ...(dto.duration !== undefined ? { duration: dto.duration } : {}),
        ...(dto.roomId !== undefined ? { roomId: dto.roomId } : {}),
        cancelComment: training.cancelComment
          ? `${training.cancelComment}\n${rescheduleNote}`
          : rescheduleNote,
      },
      include: TRAINING_INCLUDE,
    });

    this.eventEmitter.emit('training.rescheduled', { clubId, data: rescheduled });

    return this.transformTraining(rescheduled);
  }

  /**
   * Mark training as COMPLETED. Update client.lastActivityAt.
   */
  async complete(clubId: string, id: string) {
    const training = await this.ensureTrainingExists(clubId, id);

    if (training.status === 'CANCELLED') {
      throw new BadRequestException(
        'Cancelled trainings cannot be completed',
      );
    }

    const completed = await this.prisma.$transaction(async (tx) => {
      const updated = await tx.training.update({
        where: { id },
        data: { status: 'COMPLETED' },
        include: TRAINING_INCLUDE,
      });

      await tx.client.update({
        where: { id: training.clientId },
        data: { lastActivityAt: new Date() },
      });

      return updated;
    });

    this.eventEmitter.emit('training.completed', { clubId, data: completed });

    return this.transformTraining(completed);
  }

  /**
   * Confirm a training (set status to CONFIRMED).
   */
  async confirm(clubId: string, id: string) {
    const training = await this.ensureTrainingExists(clubId, id);

    if (training.status !== 'PLANNED') {
      throw new BadRequestException(
        'Only PLANNED trainings can be confirmed',
      );
    }

    const confirmed = await this.prisma.training.update({
      where: { id },
      data: { status: 'CONFIRMED' },
      include: TRAINING_INCLUDE,
    });

    return this.transformTraining(confirmed);
  }

  /**
   * Get all trainings for a specific date, optionally filtered by trainer.
   */
  async getByDate(clubId: string, date: string, trainerId?: string) {
    const dayStart = new Date(`${date}T00:00:00.000Z`);
    const dayEnd = new Date(`${date}T23:59:59.999Z`);

    const trainings = await this.prisma.training.findMany({
      where: {
        clubId,
        scheduledAt: { gte: dayStart, lte: dayEnd },
        ...(trainerId ? { trainerId } : {}),
      },
      orderBy: { scheduledAt: 'asc' },
      include: TRAINING_INCLUDE,
    });

    return this.transformTrainings(trainings);
  }

  /**
   * Get available time slots for a trainer on a given date.
   * Based on WorkSchedule minus already booked trainings.
   */
  async getTrainerSlots(
    clubId: string,
    trainerId: string,
    date: string,
  ): Promise<TimeSlot[]> {
    const dayStart = new Date(`${date}T00:00:00.000Z`);
    const dayEnd = new Date(`${date}T23:59:59.999Z`);

    const [workSchedules, trainings] = await Promise.all([
      this.prisma.workSchedule.findMany({
        where: {
          clubId,
          trainerId,
          date: { gte: dayStart, lte: dayEnd },
          isAvailable: true,
        },
      }),
      this.prisma.training.findMany({
        where: {
          clubId,
          trainerId,
          scheduledAt: { gte: dayStart, lte: dayEnd },
          status: { notIn: ['CANCELLED'] },
        },
        select: { scheduledAt: true, duration: true },
      }),
    ]);

    if (workSchedules.length === 0) {
      return [];
    }

    return this.calculateAvailableSlots(workSchedules, trainings, date);
  }

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

  /**
   * Create repeating trainings within a transaction.
   * The first training is the parent; subsequent ones reference it.
   */
  private async createRepeatingTrainings(
    clubId: string,
    trainerId: string,
    dto: CreateTrainingDto,
    scheduledAt: Date,
    duration: number,
  ) {
    const repeatCount = dto.repeatCount!;

    return this.prisma.$transaction(async (tx) => {
      // Create parent training
      const parent = await tx.training.create({
        data: {
          clubId,
          trainerId,
          clientId: dto.clientId,
          serviceId: dto.serviceId,
          roomId: dto.roomId,
          scheduledAt,
          duration,
          isRepeating: true,
        },
      });

      // Create child trainings (+7 days each)
      const children: any[] = [];
      for (let i = 1; i < repeatCount; i++) {
        const childDate = new Date(scheduledAt);
        childDate.setDate(childDate.getDate() + 7 * i);

        const child = await tx.training.create({
          data: {
            clubId,
            trainerId,
            clientId: dto.clientId,
            serviceId: dto.serviceId,
            roomId: dto.roomId,
            scheduledAt: childDate,
            duration,
            isRepeating: true,
            repeatParentId: parent.id,
          },
        });
        children.push(child);
      }

      // Return parent with relations
      return tx.training.findUnique({
        where: { id: parent.id },
        include: {
          ...TRAINING_INCLUDE,
          repeatChildren: {
            select: { id: true, scheduledAt: true, status: true },
            orderBy: { scheduledAt: 'asc' },
          },
        },
      });
    });
  }

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

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

    return { scheduledAt };
  }

  /**
   * Calculate available 60-minute slots from work schedule,
   * excluding times already occupied by trainings.
   */
  private calculateAvailableSlots(
    workSchedules: any[],
    trainings: any[],
    date: string,
  ): TimeSlot[] {
    const slots: TimeSlot[] = [];

    for (const ws of workSchedules) {
      const [startH, startM] = ws.startTime.split(':').map(Number);
      const [endH, endM] = ws.endTime.split(':').map(Number);

      const wsStart = new Date(`${date}T00:00:00.000Z`);
      wsStart.setUTCHours(startH, startM, 0, 0);

      const wsEnd = new Date(`${date}T00:00:00.000Z`);
      wsEnd.setUTCHours(endH, endM, 0, 0);

      // Generate 60-minute slots within the work schedule window
      let current = new Date(wsStart);
      while (current.getTime() + 60 * 60 * 1000 <= wsEnd.getTime()) {
        const slotEnd = new Date(current.getTime() + 60 * 60 * 1000);

        const isOccupied = trainings.some((t) => {
          const tStart = new Date(t.scheduledAt).getTime();
          const tEnd = tStart + t.duration * 60 * 1000;
          return current.getTime() < tEnd && slotEnd.getTime() > tStart;
        });

        if (!isOccupied) {
          slots.push({
            startTime: this.formatTime(current),
            endTime: this.formatTime(slotEnd),
          });
        }

        current = new Date(current.getTime() + 60 * 60 * 1000);
      }
    }

    return slots;
  }

  /**
   * Format a Date to HH:MM string.
   */
  private formatTime(date: Date): string {
    return `${date.getUTCHours().toString().padStart(2, '0')}:${date.getUTCMinutes().toString().padStart(2, '0')}`;
  }

  /**
   * Transform a training record to include date, startTime, endTime
   * computed from scheduledAt and duration.
   */
  private transformTraining(training: any): any {
    if (!training) return training;
    const scheduled = new Date(training.scheduledAt);
    const date = scheduled.toISOString().split('T')[0];
    const startTime = this.formatTime(scheduled);
    const endDate = new Date(scheduled.getTime() + (training.duration ?? 60) * 60 * 1000);
    const endTime = this.formatTime(endDate);
    return { ...training, date, startTime, endTime };
  }

  /**
   * Transform an array of training records.
   */
  private transformTrainings(trainings: any[]): any[] {
    return trainings.map((t) => this.transformTraining(t));
  }

  /**
   * Ensure a training exists within the given club, or throw NotFoundException.
   * Returns the training record for status checks.
   */
  private async ensureTrainingExists(clubId: string, id: string) {
    const training = await this.prisma.training.findFirst({
      where: { id, clubId },
    });

    if (!training) {
      throw new NotFoundException(`Training with id ${id} not found`);
    }

    return training;
  }
}
