import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';

export interface TimeSlot {
  start: string; // "09:00"
  end: string; // "10:00"
}

export interface AvailableDay {
  date: string; // "2026-03-01"
  slots: number; // count of available slots
}

@Injectable()
export class AvailabilityService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Get available dates for a service+staff combination within a date range.
   */
  async getAvailableDates(params: {
    clubId: string;
    serviceId: string;
    staffId: string;
    from: Date;
    to: Date;
  }): Promise<AvailableDay[]> {
    const { clubId, serviceId, staffId, from, to } = params;

    const [service, schedule, exceptions, existingRecords] = await Promise.all([
      this.prisma.bookingService.findUniqueOrThrow({ where: { id: serviceId } }),
      this.prisma.bookingStaffSchedule.findMany({
        where: { clubId, staffId, isActive: true },
      }),
      this.prisma.bookingStaffException.findMany({
        where: { clubId, staffId, date: { gte: from, lte: to } },
      }),
      this.prisma.bookingRecord.findMany({
        where: {
          clubId,
          staffId,
          startAt: { gte: from, lte: to },
          status: { notIn: ['CANCELLED', 'NO_SHOW'] },
        },
        select: { startAt: true, endAt: true },
      }),
    ]);

    const totalDuration = service.bufferBefore + service.duration + service.bufferAfter;
    const scheduleMap = new Map(schedule.map((s) => [s.dayOfWeek, s]));
    const exceptionMap = new Map(exceptions.map((e) => [dateToStr(e.date), e]));

    const results: AvailableDay[] = [];
    const current = new Date(from);

    while (current <= to) {
      const dateStr = dateToStr(current);
      const dayOfWeek = getDayOfWeek(current);
      const exception = exceptionMap.get(dateStr);

      let workStart: string | null = null;
      let workEnd: string | null = null;
      let breakStart: string | null = null;
      let breakEnd: string | null = null;

      if (exception) {
        if (!exception.isWorkday) {
          // Day off
          current.setDate(current.getDate() + 1);
          continue;
        }
        workStart = exception.startTime;
        workEnd = exception.endTime;
      } else {
        const sched = scheduleMap.get(dayOfWeek);
        if (!sched) {
          current.setDate(current.getDate() + 1);
          continue;
        }
        workStart = sched.startTime;
        workEnd = sched.endTime;
        breakStart = sched.breakStart;
        breakEnd = sched.breakEnd;
      }

      if (!workStart || !workEnd) {
        current.setDate(current.getDate() + 1);
        continue;
      }

      const dayRecords = existingRecords.filter((r) => dateToStr(r.startAt) === dateStr);

      const slots = this.computeSlots(
        workStart,
        workEnd,
        breakStart,
        breakEnd,
        totalDuration,
        dayRecords,
      );

      if (slots.length > 0) {
        results.push({ date: dateStr, slots: slots.length });
      }

      current.setDate(current.getDate() + 1);
    }

    return results;
  }

  /**
   * Get available time slots for a specific date.
   */
  async getAvailableSlots(params: {
    clubId: string;
    serviceId: string;
    staffId: string;
    date: Date;
    excludeRecordId?: string;
  }): Promise<TimeSlot[]> {
    const { clubId, serviceId, staffId, date, excludeRecordId } = params;

    const dateStr = dateToStr(date);
    const dayStart = new Date(dateStr + 'T00:00:00');
    const dayEnd = new Date(dateStr + 'T23:59:59');

    const [service, schedule, exception, existingRecords] = await Promise.all([
      this.prisma.bookingService.findUniqueOrThrow({ where: { id: serviceId } }),
      this.prisma.bookingStaffSchedule.findFirst({
        where: { clubId, staffId, dayOfWeek: getDayOfWeek(date), isActive: true },
      }),
      this.prisma.bookingStaffException.findFirst({
        where: { clubId, staffId, date: new Date(dateStr) },
      }),
      this.prisma.bookingRecord.findMany({
        where: {
          clubId,
          staffId,
          startAt: { gte: dayStart, lte: dayEnd },
          status: { notIn: ['CANCELLED', 'NO_SHOW'] },
          ...(excludeRecordId ? { id: { not: excludeRecordId } } : {}),
        },
        select: { startAt: true, endAt: true },
      }),
    ]);

    let workStart: string | null = null;
    let workEnd: string | null = null;
    let breakStart: string | null = null;
    let breakEnd: string | null = null;

    if (exception) {
      if (!exception.isWorkday) return [];
      workStart = exception.startTime;
      workEnd = exception.endTime;
    } else if (schedule) {
      workStart = schedule.startTime;
      workEnd = schedule.endTime;
      breakStart = schedule.breakStart;
      breakEnd = schedule.breakEnd;
    }

    if (!workStart || !workEnd) return [];

    const totalDuration = service.bufferBefore + service.duration + service.bufferAfter;

    return this.computeSlots(
      workStart,
      workEnd,
      breakStart,
      breakEnd,
      totalDuration,
      existingRecords,
    );
  }

  /**
   * Check if a specific time slot is available.
   */
  async isSlotAvailable(params: {
    clubId: string;
    staffId: string;
    startAt: Date;
    endAt: Date;
    excludeRecordId?: string;
  }): Promise<boolean> {
    const { clubId, staffId, startAt, endAt, excludeRecordId } = params;

    const conflict = await this.prisma.bookingRecord.findFirst({
      where: {
        clubId,
        staffId,
        status: { notIn: ['CANCELLED', 'NO_SHOW'] },
        ...(excludeRecordId ? { id: { not: excludeRecordId } } : {}),
        OR: [{ startAt: { lt: endAt }, endAt: { gt: startAt } }],
      },
      select: { id: true },
    });

    return !conflict;
  }

  // ─── Internal ──────────────────────────────────────────────

  private computeSlots(
    workStart: string,
    workEnd: string,
    breakStart: string | null,
    breakEnd: string | null,
    durationMin: number,
    existingRecords: { startAt: Date; endAt: Date }[],
  ): TimeSlot[] {
    const slots: TimeSlot[] = [];
    const step = 15; // 15-minute steps

    let cursor = timeToMinutes(workStart);
    const end = timeToMinutes(workEnd);
    const bStart = breakStart ? timeToMinutes(breakStart) : null;
    const bEnd = breakEnd ? timeToMinutes(breakEnd) : null;

    while (cursor + durationMin <= end) {
      const slotStart = cursor;
      const slotEnd = cursor + durationMin;

      // Skip if overlaps with break
      if (bStart !== null && bEnd !== null) {
        if (slotStart < bEnd && slotEnd > bStart) {
          cursor = bEnd;
          continue;
        }
      }

      // Check existing records
      const startStr = minutesToTime(slotStart);
      const endStr = minutesToTime(slotEnd);
      const hasConflict = existingRecords.some((r) => {
        const rStart = r.startAt.getHours() * 60 + r.startAt.getMinutes();
        const rEnd = r.endAt.getHours() * 60 + r.endAt.getMinutes();
        return slotStart < rEnd && slotEnd > rStart;
      });

      if (!hasConflict) {
        slots.push({ start: startStr, end: endStr });
      }

      cursor += step;
    }

    return slots;
  }
}

// ─── Helpers ───────────────────────────────────────────────

function dateToStr(date: Date): string {
  return date.toISOString().slice(0, 10);
}

/** Monday=0 ... Sunday=6 */
function getDayOfWeek(date: Date): number {
  const day = date.getDay(); // 0=Sun, 1=Mon...
  return day === 0 ? 6 : day - 1;
}

function timeToMinutes(time: string): number {
  const [h, m] = time.split(':').map(Number);
  return h * 60 + m;
}

function minutesToTime(minutes: number): string {
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
