import mongoose from 'mongoose';
import { DateTime } from 'luxon';
import { Types } from 'mongoose';

import {
  SalonSchema,
  ServiceSchema,
  StylistSchema,
  ClientSchema,
  WorkingHourSchema,
  TimeOffSchema,
  AppointmentSchema,
  OfferSchema,
} from './test.schema';

interface AppliedOffer {
  offerId: Types.ObjectId;
  title: string;
  discountType?: string;
  discountValue?: number;
  giftItem?: string;
}

async function main() {
  try {
    await mongoose.connect('mongodb://localhost:27017/SmartReceptionist-local');

    const SalonModel = mongoose.model('Salon', SalonSchema, 'salons');
    const ServiceModel = mongoose.model('Service', ServiceSchema, 'services');
    const StylistModel = mongoose.model('Stylist', StylistSchema, 'stylists');
    const ClientModel = mongoose.model('Client', ClientSchema, 'clients');
    const WorkingHourModel = mongoose.model(
      'Working_Hour',
      WorkingHourSchema,
      'working_hours',
    );
    const TimeOffModel = mongoose.model('TimeOff', TimeOffSchema, 'timeoffs');
    const AppointmentModel = mongoose.model(
      'Appointment',
      AppointmentSchema,
      'appointments',
    );

    const OfferModel = mongoose.model('Offer', OfferSchema, 'offers');

    async function getSalonInfo(
      salonId: string,
      fields: string[] = ['name', 'address', 'description'],
    ) {
      if (!Types.ObjectId.isValid(salonId))
        return { success: false, reason: 'Invalid salon ID' };

      try {
        const projection = Object.fromEntries(fields.map((f) => [f, 1]));
        const salon = await SalonModel.findById(
          new Types.ObjectId(salonId),
          projection,
        ).lean();
        if (!salon) return { success: false, reason: 'Salon not found' };

        return { success: true, salon };
      } catch {
        return { success: false, reason: 'Database error' };
      }
    }

    async function getServices(
      salonId?: string, // optional: filter by salon
      fields: string[] = ['name', 'duration', 'price'], // default fields
    ) {
      try {
        const filter: Record<string, any> = {};
        if (salonId) {
          if (!Types.ObjectId.isValid(salonId))
            return { success: false, reason: 'Invalid salon ID' };
          filter.salonId = new Types.ObjectId(salonId);
        }

        const projection = Object.fromEntries(fields.map((f) => [f, 1]));
        const services = await ServiceModel.find(filter, projection).lean();

        return { success: true, services };
      } catch (err) {
        console.error('[getServices] Error fetching services:', err);
        return { success: false, reason: 'Database error' };
      }
    }

    async function getNextThreeSlots(
      salon: any,
      stylists: any[],
      duration: number,
      requestedStart: DateTime,
    ) {
      const MAX_SUGGESTIONS = 3;
      const SLOT_INCREMENT = 30; // minutes
      const MIN_BUFFER_MINUTES = 30; // avoid immediate bookings

      const suggestions: {
        stylistId: Types.ObjectId;
        start: string;
        end: string;
      }[] = [];
      const now = DateTime.now().setZone(salon.timezone);
      let slotStart =
        requestedStart < now.plus({ minutes: MIN_BUFFER_MINUTES })
          ? now.plus({ minutes: MIN_BUFFER_MINUTES })
          : requestedStart;

      while (suggestions.length < MAX_SUGGESTIONS) {
        const slotEnd = slotStart.plus({ minutes: duration });
        const dayOfWeek = slotStart.weekday;
        const startTimeStr = slotStart.toFormat('HH:mm');
        const endTimeStr = slotEnd.toFormat('HH:mm');

        for (const stylist of stylists) {
          const stylistId = new Types.ObjectId(stylist._id);

          const workingHours = await WorkingHourModel.findOne({
            stylistId,
            dayOfWeek,
          });
          if (!workingHours) {
            console.log(`⛔ No working hours for day ${dayOfWeek}`);
            continue;
          }

          console.log(`⏱️ Working hours found`, {
            startTime: workingHours.startTime,
            endTime: workingHours.endTime,
            breaks: workingHours.breaks || [],
          });

          if (
            !workingHours.startTime ||
            !workingHours.endTime ||
            startTimeStr < workingHours.startTime ||
            endTimeStr > workingHours.endTime
          ) {
            console.log(`🟨 Outside working hours for stylist`);
            continue;
          }

          const conflictsWithBreak = workingHours.breaks?.some(
            (b) =>
              b.start && b.end && startTimeStr < b.end && endTimeStr > b.start,
          );
          if (conflictsWithBreak) continue;

          const isTimeOff = await TimeOffModel.findOne({
            stylistId,
            date: slotStart.toISODate(),
          });
          if (isTimeOff) continue;

          const overlappingAppointment = await AppointmentModel.findOne({
            stylistId,
            salonId: salon._id,
            status: { $in: ['confirmed', 'pending'] },
            start: { $lt: slotEnd.toJSDate() },
            end: { $gt: slotStart.toJSDate() },
          });
          if (overlappingAppointment) continue;

          suggestions.push({
            stylistId,
            start: slotStart.toISO(),
            end: slotEnd.toISO(),
          });

          if (suggestions.length >= MAX_SUGGESTIONS) break;
        }

        slotStart = slotStart.plus({ minutes: SLOT_INCREMENT });
      }

      return suggestions;
    }
    // ✅ Copy the checkAvailability logic here
    async function checkAvailability(
      salonId: string,
      serviceId: string,
      requestedStart: string,
    ) {
      console.log(`📍 Checking availability`, {
        salonId,
        serviceId,
        requestedStart,
      });

      const salon = await SalonModel.findById(salonId);
      if (!salon) {
        console.warn(`❌ Invalid salon: ${salonId}`);
        return { available: false, reason: 'Invalid salon' };
      }

      const service = await ServiceModel.findById(serviceId);
      if (!service) {
        console.warn(`❌ Invalid service: ${serviceId}`);
        return { available: false, reason: 'Invalid service' };
      }

      const duration = service.duration;
      const start = DateTime.fromISO(requestedStart, { zone: salon.timezone });
      const end = start.plus({ minutes: duration });

      console.log(`🕒 Time Window`, {
        timezone: salon.timezone,
        start: start.toISO(),
        end: end.toISO(),
        duration,
      });

      // 1️⃣ Get eligible stylists
      const stylists = await StylistModel.find({
        salonId,
        services: { $in: [new Types.ObjectId(serviceId)] },
        active: true,
      });

      console.log(`👩‍🦰 Eligible stylists count: ${stylists.length}`);

      if (!stylists.length) {
        return {
          available: false,
          reason: 'No stylist found for this service',
        };
      }

      const dayOfWeek = start.weekday;
      const startTimeStr = start.toFormat('HH:mm');
      const endTimeStr = end.toFormat('HH:mm');

      for (const stylist of stylists) {
        const stylistId = new Types.ObjectId(stylist._id);
        console.log(`\n🔍 Checking stylist: ${stylist.name} (${stylistId})`);

        // 2️⃣ Working hours
        const workingHours = await WorkingHourModel.findOne({
          stylistId,
          dayOfWeek,
        });

        if (!workingHours) {
          console.log(`⛔ No working hours for day ${dayOfWeek}`);
          continue;
        }

        console.log(`⏱️ Working hours found`, {
          startTime: workingHours.startTime,
          endTime: workingHours.endTime,
          breaks: workingHours.breaks || [],
        });

        if (
          !workingHours.startTime ||
          !workingHours.endTime ||
          startTimeStr < workingHours.startTime ||
          endTimeStr > workingHours.endTime
        ) {
          console.log(`🟨 Outside working hours for stylist`);
          continue;
        }

        // Break check
        const conflictsWithBreak = workingHours.breaks?.some((b) => {
          const conflict =
            b.start && b.end && startTimeStr < b.end && endTimeStr > b.start;
          if (conflict) {
            console.log(`🥤 Conflicts with break:`, b);
          }
          return conflict;
        });

        if (conflictsWithBreak) continue;

        // 3️⃣ Time off
        const isTimeOff = await TimeOffModel.findOne({
          stylistId,
          date: start.toISODate(),
        });

        if (isTimeOff) {
          console.log(`🚫 Stylist on time-off`);
          continue;
        }

        // 4️⃣ Appointment overlap
        const overlappingAppointment = await AppointmentModel.findOne({
          stylistId,
          salonId,
          status: { $in: ['confirmed', 'pending'] },
          start: { $lt: end.toJSDate() },
          end: { $gt: start.toJSDate() },
        });

        if (overlappingAppointment) {
          console.log(`📅 Overlapping appointment exists`, {
            appointmentId: overlappingAppointment._id,
            start: overlappingAppointment.start,
            end: overlappingAppointment.end,
          });
          continue;
        }

        console.log(`✅ Available with stylist: ${stylist.name}`);
        return {
          available: true,
          stylistId,
          start: start.toISO(),
          end: end.toISO(),
        };
      }

      console.log(`❗ No direct availability → Fetching suggestions`);

      // 5️⃣ Suggestions
      const suggestions = await getNextThreeSlots(
        salon,
        stylists,
        duration,
        start,
      );

      console.log(`📮 Suggestions returned`, { count: suggestions.length });

      return {
        available: false,
        suggestions,
      };
    }

    async function bookAppointment(
      salonId: string,
      serviceId: string,
      requestedStart: string,
      clientId: string,
    ) {
      // 1️⃣ Check availability first
      const availability = await checkAvailability(
        salonId,
        serviceId,
        requestedStart,
      );

      if (!availability.available) {
        return {
          success: false,
          reason: 'Requested time not available',
          suggestions: availability.suggestions || [],
        };
      }

      const { stylistId, start, end } = availability;

      // 2️⃣ Create appointment
      const appointment = await AppointmentModel.create({
        salonId: new Types.ObjectId(salonId),
        serviceId: new Types.ObjectId(serviceId),
        stylistId: stylistId,
        clientId: new Types.ObjectId(clientId),
        start: DateTime.fromISO(start).toJSDate(),
        end: DateTime.fromISO(end).toJSDate(),
        status: 'confirmed',
        createdAt: new Date(),
      });

      return { success: true, appointment };
    }

    async function getClientBookings(clientId: string) {
      try {
        const bookings = await AppointmentModel.find({
          clientId: new Types.ObjectId(clientId),
        })
          .select('_id start end status serviceId stylistId')
          .populate({ path: 'serviceId', select: 'name' })
          .populate({ path: 'stylistId', select: 'name' })
          .sort({ start: 1 })
          .lean();

        // Map to minimal structure
        const result = bookings.map((b: any) => ({
          bookingId: b._id,
          serviceName: b.serviceId?.name,
          stylistName: b.stylistId?.name,
          start: b.start,
          end: b.end,
          status: b.status,
        }));

        return { success: true, bookings: result };
      } catch (err) {
        this.logger.error(
          `Failed to fetch bookings for client ${clientId}`,
          err,
        );
        return { success: false, reason: 'Could not fetch bookings' };
      }
    }

    async function modifyBooking(
      bookingId: string,
      salonId: string,
      updates: { date?: string; time?: string; delete?: boolean },
    ) {
      try {
        const booking = await AppointmentModel.findOne({
          _id: new Types.ObjectId(bookingId),
          salonId: new Types.ObjectId(salonId),
        })
          .populate({ path: 'serviceId', select: 'name' })
          .populate({ path: 'stylistId', select: 'name' });

        if (!booking)
          return { success: false, reason: 'Booking not found for this salon' };

        if (updates.delete) {
          await booking.deleteOne();
          return { success: true, deleted: true };
        }

        if (updates.date && updates.time) {
          const newStart = DateTime.fromISO(`${updates.date}T${updates.time}`);
          if (!newStart.isValid)
            return { success: false, reason: 'Invalid date/time' };

          // const duration = booking.end.getTime() - booking.start.getTime();
          booking.start = newStart.toJSDate();
          // booking.end = new Date(newStart.toMillis() + duration);
        }

        await booking.save();

        return {
          success: true,
          booking: {
            bookingId: booking._id,
            serviceName: (booking.serviceId as any)?.name,
            stylistName: (booking.stylistId as any)?.name,
            start: booking.start,
            // end: booking.end,
            status: booking.status,
          },
        };
      } catch (err) {
        this.logger.error(
          `Failed to modify booking ${bookingId} for salon ${salonId}`,
          err,
        );
        return { success: false, reason: 'Could not modify booking' };
      }
    }

    async function getApplicableOffers(
      clientId: string,
      salonId: string,
      bookingDate: Date,
      bookingAmount: number,
      couponCode?: string,
    ): Promise<AppliedOffer[]> {
      const salonObjectId = new Types.ObjectId(salonId);
      const clientObjectId = new Types.ObjectId(clientId);

      // Fetch client data once
      const client = await ClientModel.findById(clientObjectId)
        .select('dateOfBirth')
        .lean();
      if (!client) return [];

      const today = bookingDate;

      return OfferModel.aggregate<AppliedOffer>([
        { $match: { salonId: salonObjectId, isActive: true } },

        // Lookup confirmed appointments once
        {
          $lookup: {
            from: 'appointments',
            let: { clientId: clientObjectId, salonId: salonObjectId },
            pipeline: [
              {
                $match: {
                  $expr: {
                    $and: [
                      { $eq: ['$clientId', '$$clientId'] },
                      { $eq: ['$salonId', '$$salonId'] },
                      { $eq: ['$status', 'confirmed'] },
                    ],
                  },
                },
              },
            ],
            as: 'confirmedAppointments',
          },
        },

        // Compute needed fields
        {
          $addFields: {
            confirmedVisitCount: { $size: '$confirmedAppointments' },
            birthdayDiff: client.dateOfBirth
              ? {
                $abs: {
                  $divide: [
                    {
                      $subtract: [
                        {
                          $dateFromParts: {
                            year: { $year: today },
                            month: { $month: client.dateOfBirth },
                            day: { $dayOfMonth: client.dateOfBirth },
                          },
                        },
                        today,
                      ],
                    },
                    1000 * 60 * 60 * 24,
                  ],
                },
              }
              : null,
          },
        },

        // Filter applicable offers
        {
          $match: {
            $or: [
              {
                $and: [
                  { type: 'nth_visit' },
                  { $expr: { $eq: ['$triggerValue', '$confirmedVisitCount'] } },
                ],
              },
              {
                $and: [
                  { type: 'birthday' },
                  { birthdayDiff: { $ne: null } }, // <-- add this line
                  { $expr: { $lte: ['$birthdayDiff', '$triggerValue'] } },
                ],
              },
              {
                $and: [
                  { type: 'coupon' },
                  couponCode ? { couponCode: couponCode.toUpperCase() } : {},
                ],
              },
              {
                $and: [
                  { type: 'salon_day' },
                  {
                    $expr: {
                      $eq: [
                        '$triggerValue',
                        { $dateToString: { date: today, format: '%Y-%m-%d' } },
                      ],
                    },
                  },
                ],
              },
              {
                $and: [
                  { type: { $in: ['spend_threshold', 'gift'] } },
                  { minSpend: { $lte: bookingAmount } },
                ],
              },
            ],
          },
        },

        // Project minimal fields
        {
          $project: {
            offerId: '$_id',
            title: 1,
            discountType: 1,
            discountValue: 1,
            giftItem: 1,
          },
        },
      ]);
    }

    // const res = await checkAvailability(
    //   '68fb7046ca5266fa52016a34',
    //   '68fb7046ca5266fa52016a37',
    //   '2025-10-24T12:25:42Z', // '2025-01-03T10:00:00Z',
    // );

    // const res = await bookAppointment(
    //   '68fb7046ca5266fa52016a34',
    //   '68fb7046ca5266fa52016a36',
    //   '2025-01-03T10:00:00Z',
    //   '68fb7046ca5266fa52016a44',
    // );

    // const res = await getSalonInfo('68fb7046ca5266fa52016a34');

    // const res = await getServices('68fb7046ca5266fa52016a34');
    // const res = await getClientBookings('68fb7046ca5266fa52016a44');

    // const res = await modifyBooking(
    //   '68fc75512dd0943ad7f69428',
    //   '68fb7046ca5266fa52016a34',
    //   { date: '2025-10-29', time: '11:30', delete: false },
    // );

    const res = await getApplicableOffers(
      '68fb7046ca5266fa52016a44',
      '68fb7046ca5266fa52016a34',
      new Date('2025-10-29'),
      20,
      'FALL2025',
    );
    console.log('RESULT:', res);
  } catch (err) {
    console.error('❌ ERROR:', err);
  } finally {
    await mongoose.disconnect();
    process.exit(0);
  }
}

main();

// npx ts-node test-booking.ts
