import Appointment, { IAppointment } from '../models/Appointment';
import NotificationLog, { NotificationPurpose, NotificationStatus, NotificationType } from '../models/NotificationLog';
import Patient from '../models/Patient';
import EmailService from './EmailService';
import NotificationQueueService from './NotificationQueueService';
import WhatsAppService from './WhatsAppService';

export class AppointmentService {
  private async assertPatientHasEmail(patientId: any): Promise<void> {
    const patient = await Patient.findById(patientId).populate({
      path: 'userId',
      select: 'email'
    });

    if (!patient) {
      throw new Error('Patient not found');
    }

    const email = (patient.userId as any)?.email;
    if (!email || !String(email).trim()) {
      throw new Error('Patient email is required for appointment notifications');
    }
  }

  private normalizeAppointmentStatus(status?: string): IAppointment['status'] | undefined {
    if (!status) return undefined;

    const normalized = status.trim().toLowerCase();
    if (normalized === 'scheduled') return 'Scheduled';
    if (normalized === 'completed') return 'Completed';
    if (normalized === 'cancelled' || normalized === 'canceled') return 'Cancelled';
    return undefined;
  }

  // ─── Read Operations ───

  async getAllAppointments(): Promise<IAppointment[]> {
    return await Appointment.find({ patientId: { $ne: null }, doctorId: { $ne: null } })
      .populate({
        path: 'patientId',
        populate: { path: 'userId' }
      })
      .populate({
        path: 'doctorId',
        populate: { path: 'userId' }
      });
  }

  async getAppointmentById(id: string): Promise<IAppointment | null> {
    return await Appointment.findById(id)
      .populate({
        path: 'patientId',
        populate: { path: 'userId' }
      })
      .populate({
        path: 'doctorId',
        populate: { path: 'userId' }
      });
  }

  async getAppointmentsByPatient(patientId: string): Promise<IAppointment[]> {
    return await Appointment.find({ patientId })
      .populate({
        path: 'patientId',
        populate: { path: 'userId' }
      })
      .populate({
        path: 'doctorId',
        populate: { path: 'userId' }
      });
  }

  async getAppointmentsByDoctor(doctorId: string): Promise<IAppointment[]> {
    return await Appointment.find({ doctorId })
      .populate({
        path: 'patientId',
        populate: { path: 'userId' }
      })
      .populate({
        path: 'doctorId',
        populate: { path: 'userId' }
      });
  }

  async getAppointmentsByStatus(status: string): Promise<IAppointment[]> {
    return await Appointment.find({ status })
      .populate({ path: 'patientId', populate: { path: 'userId' } })
      .populate({ path: 'doctorId', populate: { path: 'userId' } });
  }

  async getDashboardAppointments(
    limit: number = 5,
    upcoming: boolean = false,
    doctorId?: string,
    patientId?: string,
    doctorIds?: string[]
  ): Promise<IAppointment[]> {
    const query: any = {};

    if (upcoming) {
      query.appointmentDate = { $gte: new Date() };
      query.status = 'Scheduled';
    }

    // Filter by team doctorIds (primary doctor viewing all team appointments)
    if (doctorIds && doctorIds.length > 0) {
      query.doctorId = { $in: doctorIds };
    } else if (doctorId) {
      // Filter by single doctorId (team member viewing own appointments)
      query.doctorId = doctorId;
    }

    if (patientId) {
      query.patientId = patientId;
    }

    return await Appointment.find(query)
      .sort({ appointmentDate: 1 }) // Soonest first
      .limit(limit)
      .populate({ path: 'patientId', populate: { path: 'userId' } })
      .populate({ path: 'doctorId', populate: { path: 'userId' } });
  }

  async getAppointmentsPaginated(params: {
    page?: number;
    limit?: number;
    doctorId?: string;
    doctorIds?: string[];
    patientId?: string;
    status?: string;
  }): Promise<{ data: IAppointment[]; total: number; page: number; limit: number }> {
    const page = Math.max(1, params.page || 1);
    const limit = Math.max(1, params.limit || 20);
    const skip = (page - 1) * limit;

    const query: any = {};
    if (params.doctorIds && params.doctorIds.length > 0) {
      query.doctorId = { $in: params.doctorIds };
    } else if (params.doctorId) {
      query.doctorId = params.doctorId;
    }
    if (params.patientId) query.patientId = params.patientId;

    const normalizedStatus = this.normalizeAppointmentStatus(params.status);
    if (normalizedStatus) {
      query.status = normalizedStatus;
    }

    const [data, total] = await Promise.all([
      Appointment.find(query)
        .sort({ createdAt: -1, _id: -1 })
        .skip(skip)
        .limit(limit)
        .populate({ path: 'patientId', populate: { path: 'userId' } })
        .populate({ path: 'doctorId', populate: { path: 'userId' } }),
      Appointment.countDocuments(query),
    ]);

    return { data, total, page, limit };
  }

  // ─── Create Appointment ───
  // Flow: Save → Send immediate confirmation email → Schedule delayed reminders → Store job IDs

  async createAppointment(appointmentData: Partial<IAppointment>): Promise<IAppointment> {
    if (!appointmentData.patientId) {
      throw new Error('Patient ID is required');
    }

    await this.assertPatientHasEmail(appointmentData.patientId);

    // 1. Save appointment
    const appointment = new Appointment(appointmentData);
    const savedAppointment = await appointment.save();

    // 2. Send immediate confirmation email (fire-and-forget, don't block creation)
    this.sendConfirmationEmail(savedAppointment).catch(err => {
      console.error(`[AppointmentService] Failed to send confirmation email for ${savedAppointment._id}:`, err);
    });

    // 3. Schedule delayed reminders in queue and store job IDs
    try {
      const jobIds = await NotificationQueueService.scheduleAppointmentReminders(savedAppointment);

      // Store job IDs on the appointment for future cancellation/reschedule
      if (jobIds.length > 0) {
        await Appointment.findByIdAndUpdate(savedAppointment._id, {
          scheduledJobIds: jobIds
        });
      }
    } catch (error) {
      console.error(`[AppointmentService] Failed to schedule reminders for ${savedAppointment._id}:`, error);
      // Don't fail the appointment creation if queue scheduling fails
    }

    return savedAppointment;
  }

  // ─── Update Appointment (Reschedule) ───
  // Flow: Check if date changed → Remove old jobs → Increment version → Schedule new jobs → Store new IDs

  async updateAppointment(id: string, appointmentData: Partial<IAppointment>): Promise<IAppointment | null> {
    // 1. Fetch current appointment to get old job IDs and current status
    const currentAppointment = await Appointment.findById(id);
    if (!currentAppointment) return null;

    const nextPatientId = appointmentData.patientId || currentAppointment.patientId;
    await this.assertPatientHasEmail(nextPatientId);

    const normalizedStatus = this.normalizeAppointmentStatus(appointmentData.status as string | undefined);
    if (appointmentData.status && !normalizedStatus) {
      throw new Error(`Invalid appointment status: ${appointmentData.status}`);
    }

    const dateChanged = appointmentData.appointmentDate &&
      new Date(appointmentData.appointmentDate).toISOString() !== new Date(currentAppointment.appointmentDate).toISOString();

    const statusChanged = Boolean(normalizedStatus && normalizedStatus !== currentAppointment.status);

    // 2. Update appointment, increment notification version if date changed
    const updateData: any = { ...appointmentData };
    if (normalizedStatus) {
      updateData.status = normalizedStatus;
    }
    if (dateChanged) {
      updateData.$inc = { notificationVersion: 1 };
    }

    const updatedAppointment = await Appointment.findByIdAndUpdate(id, updateData, { new: true });
    if (!updatedAppointment) return null;

    // 3. Handle status change notifications
    if (statusChanged) {
      const newStatus = normalizedStatus;

      if (newStatus === 'Cancelled') {
        // Remove all scheduled notification jobs from Redis
        if (currentAppointment.scheduledJobIds.length > 0) {
          try {
            await NotificationQueueService.removeScheduledJobs(currentAppointment.scheduledJobIds);
            await Appointment.findByIdAndUpdate(id, { scheduledJobIds: [] });
          } catch (error) {
            console.error(`[AppointmentService] Failed to remove scheduled jobs for ${id}:`, error);
          }
        }

        // Send cancellation email (fire-and-forget)
        this.sendCancellationEmail(
          await Appointment.findById(id)
            .populate({ path: 'patientId', populate: { path: 'userId' } })
            .populate({ path: 'doctorId', populate: { path: 'userId' } }) as any
        ).catch(err => {
          console.error(`[AppointmentService] Failed to send cancellation email for ${id}:`, err);
        });

        console.log(`[AppointmentService] Appointment ${id} cancelled via status change`);
        return updatedAppointment;
      }

      if (newStatus === 'Completed') {
        // Clean up scheduled reminders since appointment is done
        if (currentAppointment.scheduledJobIds.length > 0) {
          try {
            await NotificationQueueService.removeScheduledJobs(currentAppointment.scheduledJobIds);
            await Appointment.findByIdAndUpdate(id, { scheduledJobIds: [] });
            console.log(`[AppointmentService] Cleaned up scheduled jobs for completed appointment ${id}`);
          } catch (error) {
            console.error(`[AppointmentService] Failed to remove scheduled jobs for ${id}:`, error);
          }
        }

        // Send completed email (fire-and-forget)
        this.sendCompletedEmail(
          await Appointment.findById(id)
            .populate({ path: 'patientId', populate: { path: 'userId' } })
            .populate({ path: 'doctorId', populate: { path: 'userId' } }) as any
        ).catch(err => {
          console.error(`[AppointmentService] Failed to send completed email for ${id}:`, err);
        });
      }
    }

    // 4. If date changed (and not cancelled), handle notification reschedule
    if (dateChanged && updatedAppointment.status !== 'Cancelled') {
      // Send reschedule email only for active appointments
      if (updatedAppointment.status === 'Scheduled') {
        this.sendRescheduledEmail(
          updatedAppointment,
          new Date(currentAppointment.appointmentDate)
        ).catch(err => {
          console.error(`[AppointmentService] Failed to send rescheduled email for ${id}:`, err);
        });
      }

      try {
        // Remove old delayed jobs from Redis
        if (currentAppointment.scheduledJobIds.length > 0) {
          await NotificationQueueService.removeScheduledJobs(currentAppointment.scheduledJobIds);
        }

        // Schedule new jobs with updated version
        const newJobIds = await NotificationQueueService.scheduleAppointmentReminders(updatedAppointment);

        // Update appointment with new job IDs
        await Appointment.findByIdAndUpdate(id, {
          scheduledJobIds: newJobIds
        });

        console.log(`[AppointmentService] Rescheduled notifications for appointment ${id}`);
      } catch (error) {
        console.error(`[AppointmentService] Failed to reschedule notifications for ${id}:`, error);
      }
    }

    return updatedAppointment;
  }

  // ─── Cancel Appointment ───
  // Flow: Update status → Remove queued jobs → Send cancellation email (idempotent)

  async cancelAppointment(id: string, cancelledBy?: string): Promise<IAppointment | null> {
    // 1. Fetch appointment first
    const appointment = await Appointment.findById(id)
      .populate({ path: 'patientId', populate: { path: 'userId' } })
      .populate({ path: 'doctorId', populate: { path: 'userId' } });

    if (!appointment) return null;

    // Idempotency: if already cancelled, return early
    if (appointment.status === 'Cancelled') {
      console.log(`[AppointmentService] Appointment ${id} already cancelled`);
      return appointment;
    }

    // 2. Update status to Cancelled
    appointment.status = 'Cancelled';
    await appointment.save();

    // 3. Remove all scheduled notification jobs from Redis
    if (appointment.scheduledJobIds.length > 0) {
      try {
        await NotificationQueueService.removeScheduledJobs(appointment.scheduledJobIds);
        // Clear stored job IDs
        await Appointment.findByIdAndUpdate(id, { scheduledJobIds: [] });
      } catch (error) {
        console.error(`[AppointmentService] Failed to remove scheduled jobs for ${id}:`, error);
      }
    }

    // 4. Send immediate cancellation email (idempotent)
    this.sendCancellationEmail(appointment, cancelledBy).catch(err => {
      console.error(`[AppointmentService] Failed to send cancellation email for ${id}:`, err);
    });

    return appointment;
  }

  // ─── Delete Appointment ───

  async deleteAppointment(id: string): Promise<IAppointment | null> {
    const appointment = await Appointment.findById(id);
    if (!appointment) return null;

    // Remove queued jobs before deleting
    if (appointment.scheduledJobIds.length > 0) {
      await NotificationQueueService.removeScheduledJobs(appointment.scheduledJobIds).catch(() => { });
    }

    return await Appointment.findByIdAndDelete(id);
  }

  // ─── Private: Send Confirmation Email ───

  private async sendConfirmationEmail(appointment: IAppointment) {
    const populated = await Appointment.findById(appointment._id)
      .populate({ path: 'patientId', populate: { path: 'userId' } })
      .populate({ path: 'doctorId', populate: { path: 'userId' } });

    if (!populated) return;

    const patient = populated.patientId as any;
    const doctor = populated.doctorId as any;

    const patientEmail = patient?.userId?.email;
    const patientPhone = patient?.userId?.phone;
    if (!patientEmail) {
      console.warn('[AppointmentService] Cannot send confirmation: patient email missing');
      return;
    }

    const patientName = `${patient.userId.firstName} ${patient.userId.lastName}`;
    const patientLang = patient.userId.language || 'en';
    const canSendEmail = Boolean(patientEmail);
    const canSendWhatsApp = Boolean(patientPhone && WhatsAppService.isEnabled());
    const doctorName = doctor?.userId?.firstName
      ? `${doctor.userId.firstName} ${doctor.userId.lastName}`
      : 'Your Doctor';
    const dateStr = new Date(populated.appointmentDate).toLocaleString();

    // Idempotency keys for confirmation by channel
    const emailIdempotencyKey = `${populated._id}-${NotificationPurpose.APPOINTMENT_CONFIRMATION}-${NotificationType.EMAIL}-v${populated.notificationVersion}`;
    const whatsappIdempotencyKey = `${populated._id}-${NotificationPurpose.APPOINTMENT_CONFIRMATION}-${NotificationType.WHATSAPP}-v${populated.notificationVersion}`;

    const [existingEmail, existingWhatsApp] = await Promise.all([
      NotificationLog.findOne({ idempotencyKey: emailIdempotencyKey, status: NotificationStatus.SENT }),
      NotificationLog.findOne({ idempotencyKey: whatsappIdempotencyKey, status: NotificationStatus.SENT })
    ]);

    if (existingEmail || !canSendEmail) {
      console.log(`[AppointmentService] Confirmation email already sent for ${populated._id}`);
      return;
    }

    if (!existingEmail && canSendEmail) {
      const sent = await EmailService.sendAppointmentConfirmation(patientEmail, patientName, dateStr, doctorName, patientLang);

      await NotificationLog.findOneAndUpdate(
        { idempotencyKey: emailIdempotencyKey },
        {
          appointmentId: populated._id,
          userId: patient.userId._id,
          jobId: `confirmation-${populated._id}`,
          type: NotificationType.EMAIL,
          purpose: NotificationPurpose.APPOINTMENT_CONFIRMATION,
          status: sent ? NotificationStatus.SENT : NotificationStatus.FAILED,
          sentAt: sent ? new Date() : undefined,
          idempotencyKey: emailIdempotencyKey
        },
        { upsert: true, new: true }
      );

      console.log(`[AppointmentService] ${sent ? '✓' : '✗'} Confirmation email for appointment ${populated._id}`);
    }

    // WhatsApp send call intentionally disabled for now.
    void canSendWhatsApp;
    void existingWhatsApp;
    void whatsappIdempotencyKey;
    void patientPhone;
  }

  // ─── Private: Send Rescheduled Email ───

  private async sendRescheduledEmail(appointment: IAppointment, oldDate: Date) {
    const populated = await Appointment.findById(appointment._id)
      .populate({ path: 'patientId', populate: { path: 'userId' } })
      .populate({ path: 'doctorId', populate: { path: 'userId' } });

    if (!populated) return;

    const patient = populated.patientId as any;
    const doctor = populated.doctorId as any;

    const patientEmail = patient?.userId?.email;
    const patientPhone = patient?.userId?.phone;
    if (!patientEmail) {
      console.warn('[AppointmentService] Cannot send rescheduled notification: patient email missing');
      return;
    }

    const patientName = `${patient.userId.firstName} ${patient.userId.lastName}`;
    const patientLang = patient.userId.language || 'en';
    const canSendEmail = Boolean(patientEmail);
    const canSendWhatsApp = Boolean(patientPhone && WhatsAppService.isEnabled());
    const doctorName = doctor?.userId?.firstName
      ? `${doctor.userId.firstName} ${doctor.userId.lastName}`
      : 'Your Doctor';
    const oldDateStr = new Date(oldDate).toLocaleString();
    const newDateStr = new Date(populated.appointmentDate).toLocaleString();

    const emailIdempotencyKey = `${populated._id}-${NotificationPurpose.APPOINTMENT_RESCHEDULED}-${NotificationType.EMAIL}-v${populated.notificationVersion}`;
    const whatsappIdempotencyKey = `${populated._id}-${NotificationPurpose.APPOINTMENT_RESCHEDULED}-${NotificationType.WHATSAPP}-v${populated.notificationVersion}`;

    const [existingEmail, existingWhatsApp] = await Promise.all([
      NotificationLog.findOne({ idempotencyKey: emailIdempotencyKey, status: NotificationStatus.SENT }),
      NotificationLog.findOne({ idempotencyKey: whatsappIdempotencyKey, status: NotificationStatus.SENT })
    ]);

    if (existingEmail || !canSendEmail) {
      console.log(`[AppointmentService] Rescheduled email already sent for ${populated._id}`);
      return;
    }

    if (!existingEmail && canSendEmail) {
      const sent = await EmailService.sendAppointmentRescheduled(
        patientEmail,
        patientName,
        oldDateStr,
        newDateStr,
        doctorName,
        patientLang
      );

      await NotificationLog.findOneAndUpdate(
        { idempotencyKey: emailIdempotencyKey },
        {
          appointmentId: populated._id,
          userId: patient.userId._id,
          jobId: `rescheduled-${populated._id}`,
          type: NotificationType.EMAIL,
          purpose: NotificationPurpose.APPOINTMENT_RESCHEDULED,
          status: sent ? NotificationStatus.SENT : NotificationStatus.FAILED,
          sentAt: sent ? new Date() : undefined,
          idempotencyKey: emailIdempotencyKey
        },
        { upsert: true, new: true }
      );

      console.log(`[AppointmentService] ${sent ? '✓' : '✗'} Rescheduled email for appointment ${populated._id}`);
    }

    // WhatsApp send call intentionally disabled for now.
    void canSendWhatsApp;
    void existingWhatsApp;
    void whatsappIdempotencyKey;
    void patientPhone;
  }

  // ─── Private: Send Cancellation Email ───

  private async sendCancellationEmail(appointment: IAppointment, cancelledBy?: string) {
    const patient = appointment.patientId as any;
    const doctor = appointment.doctorId as any;

    const patientEmail = patient?.userId?.email;
    const patientPhone = patient?.userId?.phone;
    if (!patientEmail) {
      console.warn('[AppointmentService] Cannot send cancellation: patient email missing');
      return;
    }

    const patientName = `${patient.userId.firstName} ${patient.userId.lastName}`;
    const patientLang = patient.userId.language || 'en';
    const canSendEmail = Boolean(patientEmail);
    const canSendWhatsApp = Boolean(patientPhone && WhatsAppService.isEnabled());
    const doctorName = doctor?.userId?.firstName
      ? `${doctor.userId.firstName} ${doctor.userId.lastName}`
      : 'Your Doctor';
    const dateStr = new Date(appointment.appointmentDate).toLocaleString();

    // Idempotency: prevent duplicate sends on API retry
    const emailIdempotencyKey = `${appointment._id}-${NotificationPurpose.APPOINTMENT_CANCELLATION}-${NotificationType.EMAIL}`;
    const whatsappIdempotencyKey = `${appointment._id}-${NotificationPurpose.APPOINTMENT_CANCELLATION}-${NotificationType.WHATSAPP}`;

    const [existingEmail, existingWhatsApp] = await Promise.all([
      NotificationLog.findOne({ idempotencyKey: emailIdempotencyKey, status: NotificationStatus.SENT }),
      NotificationLog.findOne({ idempotencyKey: whatsappIdempotencyKey, status: NotificationStatus.SENT })
    ]);

    if (existingEmail || !canSendEmail) {
      console.log(`[AppointmentService] Cancellation email already sent for ${appointment._id}`);
      return;
    }

    if (!existingEmail && canSendEmail) {
      const sent = await EmailService.sendAppointmentCancellation(patientEmail, patientName, dateStr, doctorName, patientLang);

      await NotificationLog.findOneAndUpdate(
        { idempotencyKey: emailIdempotencyKey },
        {
          appointmentId: appointment._id,
          userId: patient.userId._id,
          jobId: `cancellation-${appointment._id}`,
          type: NotificationType.EMAIL,
          purpose: NotificationPurpose.APPOINTMENT_CANCELLATION,
          status: sent ? NotificationStatus.SENT : NotificationStatus.FAILED,
          sentAt: sent ? new Date() : undefined,
          idempotencyKey: emailIdempotencyKey
        },
        { upsert: true, new: true }
      );

      console.log(`[AppointmentService] ${sent ? '✓' : '✗'} Cancellation email for appointment ${appointment._id}`);
    }

    // WhatsApp send call intentionally disabled for now.
    void canSendWhatsApp;
    void existingWhatsApp;
    void whatsappIdempotencyKey;
    void patientPhone;
  }

  // ─── Private: Send Completed Notification ───

  private async sendCompletedEmail(appointment: IAppointment) {
    const patient = appointment.patientId as any;
    const doctor = appointment.doctorId as any;

    const patientEmail = patient?.userId?.email;
    const patientPhone = patient?.userId?.phone;
    if (!patientEmail) {
      console.warn('[AppointmentService] Cannot send completion notification: patient email missing');
      return;
    }

    const patientName = `${patient.userId.firstName} ${patient.userId.lastName}`;
    const patientLang = patient.userId.language || 'en';
    const canSendEmail = Boolean(patientEmail);
    const canSendWhatsApp = Boolean(patientPhone && WhatsAppService.isEnabled());
    const doctorName = doctor?.userId?.firstName
      ? `${doctor.userId.firstName} ${doctor.userId.lastName}`
      : 'Your Doctor';
    const dateStr = new Date(appointment.appointmentDate).toLocaleString();

    const emailIdempotencyKey = `${appointment._id}-${NotificationPurpose.APPOINTMENT_COMPLETED}-${NotificationType.EMAIL}`;
    const whatsappIdempotencyKey = `${appointment._id}-${NotificationPurpose.APPOINTMENT_COMPLETED}-${NotificationType.WHATSAPP}`;
    const [existingEmail, existingWhatsApp] = await Promise.all([
      NotificationLog.findOne({ idempotencyKey: emailIdempotencyKey, status: NotificationStatus.SENT }),
      NotificationLog.findOne({ idempotencyKey: whatsappIdempotencyKey, status: NotificationStatus.SENT })
    ]);

    if (existingEmail || !canSendEmail) {
      console.log(`[AppointmentService] Completion notification already sent for ${appointment._id}`);
      return;
    }

    if (!existingEmail && canSendEmail) {
      const sent = await EmailService.sendAppointmentCompleted(patientEmail, patientName, dateStr, doctorName, patientLang);

      await NotificationLog.findOneAndUpdate(
        { idempotencyKey: emailIdempotencyKey },
        {
          appointmentId: appointment._id,
          userId: patient.userId._id,
          jobId: `completed-${appointment._id}`,
          type: NotificationType.EMAIL,
          purpose: NotificationPurpose.APPOINTMENT_COMPLETED,
          status: sent ? NotificationStatus.SENT : NotificationStatus.FAILED,
          sentAt: sent ? new Date() : undefined,
          idempotencyKey: emailIdempotencyKey
        },
        { upsert: true, new: true }
      );

      console.log(`[AppointmentService] ${sent ? '✓' : '✗'} Completed email for appointment ${appointment._id}`);
    }

    // WhatsApp send call intentionally disabled for now.
    void canSendWhatsApp;
    void existingWhatsApp;
    void whatsappIdempotencyKey;
    void patientPhone;
  }
}

export default new AppointmentService();
