import bcrypt from 'bcryptjs';
import { Prisma, ShiftStatus, SignupStatus, SignupSource, UserRole } from '@prisma/client';
import { prisma } from '../../../config/database';
import { AppError } from '../../../middleware/error-handler';
import { emailService } from '../../../services/email.service';
import { notificationQueueService } from '../../../services/notification-queue.service';
import { getAdminEmailsByRole, isNotificationEnabled } from '../../../services/notification.helper';
import { env } from '../../../config/env';
import { logger } from '../../../utils/logger';
import { recordShiftSignup } from '../../../utils/metrics';
import { eventBus } from '../../../services/event-bus.service';
import { unifiedCalendarService } from '../../events/unified-calendar.service';
import { groupService } from '../../social/group.service';
import { achievementsService } from '../../social/achievements.service';
import { generateSlug } from '../../../utils/slug';
import { siteSettingsService } from '../../settings/settings.service';
import { smsNotificationService } from '../../../services/sms-notification.service';
import crypto from 'crypto';
import type {
  CreateShiftInput,
  UpdateShiftInput,
  ListShiftsInput,
  AddSignupInput,
  PublicSignupInput,
} from './shifts.schemas';

function generateReadablePassword(): string {
  // Generate a cryptographically strong random password (128 bits of entropy)
  return crypto.randomBytes(16).toString('base64url');
}

const meetingSelect = {
  id: true,
  slug: true,
  title: true,
  isActive: true,
  jitsiRoom: true,
} as const;

export const shiftsService = {
  async findAll(filters: ListShiftsInput) {
    const { page, limit, search, status, kind, upcoming, sortBy, sortOrder } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.ShiftWhereInput = {};

    if (search) {
      where.OR = [
        { title: { contains: search, mode: 'insensitive' } },
        { location: { contains: search, mode: 'insensitive' } },
      ];
    }

    if (status) where.status = status;
    if (kind) where.kind = kind;

    if (upcoming) {
      where.date = { gte: new Date() };
    }

    const orderBy: Prisma.ShiftOrderByWithRelationInput =
      sortBy === 'title'
        ? { title: sortOrder }
        : sortBy === 'createdAt'
          ? { createdAt: sortOrder }
          : { date: sortOrder };

    const [shifts, total] = await Promise.all([
      prisma.shift.findMany({
        where,
        skip,
        take: limit,
        orderBy,
        include: {
          cut: { select: { id: true, name: true } },
          ticketedEvent: { select: { id: true, slug: true, title: true, date: true } },
          meeting: { select: meetingSelect },
          _count: {
            select: {
              signups: { where: { status: SignupStatus.CONFIRMED } },
            },
          },
        },
      }),
      prisma.shift.count({ where }),
    ]);

    return {
      shifts,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  },

  async findById(id: string) {
    const shift = await prisma.shift.findUnique({
      where: { id },
      include: {
        cut: { select: { id: true, name: true } },
        meeting: { select: meetingSelect },
        signups: {
          where: { status: SignupStatus.CONFIRMED },
          include: { user: { select: { id: true, email: true, name: true, phone: true } } },
          orderBy: { signupDate: 'desc' },
        },
        _count: {
          select: {
            signups: { where: { status: SignupStatus.CONFIRMED } },
          },
        },
      },
    });

    if (!shift) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    return shift;
  },

  async create(data: CreateShiftInput, userId: string) {
    const shift = await prisma.shift.create({
      data: {
        title: data.title,
        description: data.description,
        date: new Date(data.date),
        startTime: data.startTime,
        endTime: data.endTime,
        location: data.location,
        maxVolunteers: data.maxVolunteers,
        isPublic: data.isPublic,
        cutId: data.cutId,
        kind: data.kind,
        ticketedEventId: data.ticketedEventId,
        createdBy: userId,
      },
    });

    // Publish shift.created event (listeners: Gancio, Calendar, n8n)
    eventBus.publish('shift.created', {
      shiftId: shift.id,
      title: shift.title,
      date: new Date(shift.date).toISOString().split('T')[0],
      startTime: shift.startTime,
      endTime: shift.endTime,
      cutId: shift.cutId,
      cutName: null,
      createdByUserId: userId,
    });

    // Bust unified calendar cache
    unifiedCalendarService.bustCache().catch(() => {});

    return shift;
  },

  async update(id: string, data: UpdateShiftInput) {
    const existing = await prisma.shift.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    const updateData: Prisma.ShiftUncheckedUpdateInput = { ...data };

    if (data.date) {
      updateData.date = new Date(data.date);
    }

    // Auto-update status when capacity changes
    if (data.maxVolunteers !== undefined) {
      if (existing.currentVolunteers >= data.maxVolunteers && existing.status === ShiftStatus.OPEN) {
        updateData.status = ShiftStatus.FULL;
      } else if (existing.currentVolunteers < data.maxVolunteers && existing.status === ShiftStatus.FULL) {
        updateData.status = ShiftStatus.OPEN;
      }
    }

    const shift = await prisma.shift.update({
      where: { id },
      data: updateData,
    });

    // Publish shift.updated event (listeners: Gancio, Calendar, n8n)
    eventBus.publish('shift.updated', {
      shiftId: shift.id,
      title: shift.title,
      date: new Date(shift.date).toISOString().split('T')[0],
      startTime: shift.startTime,
      endTime: shift.endTime,
      cutId: shift.cutId,
      cutName: null,
      changes: Object.keys(data),
    });

    // Bust unified calendar cache
    unifiedCalendarService.bustCache().catch(() => {});

    return shift;
  },

  async delete(id: string) {
    const existing = await prisma.shift.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    // Publish shift.deleted event (listeners: Gancio, Calendar, n8n)
    eventBus.publish('shift.deleted', {
      shiftId: id,
      title: existing.title,
      date: new Date(existing.date).toISOString().split('T')[0],
    });

    // Delete associated meeting if exists
    if (existing.meetingId) {
      await prisma.meeting.delete({ where: { id: existing.meetingId } }).catch(() => {});
    }

    await prisma.shift.delete({ where: { id } });

    // Bust unified calendar cache
    unifiedCalendarService.bustCache().catch(() => {});
  },

  async createMeetingForShift(shiftId: string, userId: string) {
    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });
    if (!shift) throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    if (shift.meetingId) throw new AppError(400, 'Shift already has a meeting', 'MEETING_EXISTS');

    const settings = await siteSettingsService.get();
    if (!settings.enableMeet) throw new AppError(400, 'Video meetings are not enabled', 'MEET_DISABLED');

    const meeting = await prisma.meeting.create({
      data: {
        slug: generateSlug(shift.title),
        title: `${shift.title} — Video Briefing`,
        jitsiRoom: crypto.randomUUID(),
        createdByUserId: userId,
      },
    });

    await prisma.shift.update({
      where: { id: shiftId },
      data: { meetingId: meeting.id },
    });

    return meeting;
  },

  async removeMeetingFromShift(shiftId: string) {
    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });
    if (!shift) throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    if (!shift.meetingId) throw new AppError(400, 'Shift has no meeting', 'NO_MEETING');

    const meetingId = shift.meetingId;
    await prisma.shift.update({
      where: { id: shiftId },
      data: { meetingId: null },
    });

    // Delete the meeting record
    await prisma.meeting.delete({ where: { id: meetingId } }).catch(() => {});
  },

  async getStats() {
    const [total, open, full, cancelled, upcoming, totalSignups] = await Promise.all([
      prisma.shift.count(),
      prisma.shift.count({ where: { status: ShiftStatus.OPEN } }),
      prisma.shift.count({ where: { status: ShiftStatus.FULL } }),
      prisma.shift.count({ where: { status: ShiftStatus.CANCELLED } }),
      prisma.shift.count({ where: { date: { gte: new Date() }, status: { not: ShiftStatus.CANCELLED } } }),
      prisma.shiftSignup.count({ where: { status: SignupStatus.CONFIRMED } }),
    ]);

    return { total, open, full, cancelled, upcoming, totalSignups };
  },

  async getSignups(shiftId: string) {
    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });
    if (!shift) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    return prisma.shiftSignup.findMany({
      where: { shiftId },
      include: { user: { select: { id: true, email: true, name: true, phone: true } } },
      orderBy: { signupDate: 'desc' },
    });
  },

  async addSignup(shiftId: string, data: AddSignupInput) {
    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });
    if (!shift) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    if (shift.currentVolunteers >= shift.maxVolunteers) {
      throw new AppError(400, 'Shift is full', 'SHIFT_FULL');
    }

    // Check unique constraint
    const existing = await prisma.shiftSignup.findUnique({
      where: { shiftId_userEmail: { shiftId, userEmail: data.userEmail } },
    });
    if (existing) {
      if (existing.status === SignupStatus.CANCELLED) {
        // Re-activate cancelled signup
        const [signup] = await prisma.$transaction([
          prisma.shiftSignup.update({
            where: { id: existing.id },
            data: { status: SignupStatus.CONFIRMED, signupSource: SignupSource.ADMIN },
          }),
          prisma.shift.update({
            where: { id: shiftId },
            data: {
              currentVolunteers: { increment: 1 },
              status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? ShiftStatus.FULL : undefined,
            },
          }),
        ]);
        return signup;
      }
      throw new AppError(409, 'Volunteer already signed up', 'DUPLICATE_SIGNUP');
    }

    // Look up user
    const user = await prisma.user.findUnique({ where: { email: data.userEmail } });

    const [signup] = await prisma.$transaction([
      prisma.shiftSignup.create({
        data: {
          shiftId,
          shiftTitle: shift.title,
          userId: user?.id,
          userEmail: data.userEmail,
          userName: data.userName || user?.name,
          signupSource: SignupSource.ADMIN,
        },
      }),
      prisma.shift.update({
        where: { id: shiftId },
        data: {
          currentVolunteers: { increment: 1 },
          status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? ShiftStatus.FULL : undefined,
        },
      }),
    ]);

    // Publish shift.signup.created event (listeners: Listmonk, RC, CRM, n8n)
    eventBus.publish('shift.signup.created', {
      shiftId,
      shiftTitle: shift.title,
      shiftDate: new Date(shift.date).toISOString().split('T')[0],
      userName: data.userName || data.userEmail,
      userEmail: data.userEmail,
      userId: user?.id ?? null,
      cutName: null,
      signupType: 'admin',
    });

    // Social group sync (fire-and-forget)
    groupService.syncShiftTeam(shiftId).catch(() => {});

    // Achievement check (fire-and-forget)
    if (user?.id) achievementsService.checkAndUnlock(user.id, ['shifts']).catch(() => {});

    return signup;
  },

  async removeSignup(signupId: string) {
    const signup = await prisma.shiftSignup.findUnique({ where: { id: signupId } });
    if (!signup) {
      throw new AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
    }

    if (signup.status === SignupStatus.CANCELLED) {
      throw new AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
    }

    await prisma.$transaction([
      prisma.shiftSignup.update({
        where: { id: signupId },
        data: { status: SignupStatus.CANCELLED },
      }),
      prisma.shift.update({
        where: { id: signup.shiftId },
        data: {
          currentVolunteers: { decrement: 1 },
          status: ShiftStatus.OPEN,
        },
      }),
    ]);

    // Social group sync (fire-and-forget)
    groupService.syncShiftTeam(signup.shiftId).catch(() => {});
  },

  async publicSignup(shiftId: string, data: PublicSignupInput) {
    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });
    if (!shift) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    if (!shift.isPublic) {
      throw new AppError(403, 'This shift is not open for public signup', 'NOT_PUBLIC');
    }

    if (shift.status !== ShiftStatus.OPEN) {
      throw new AppError(400, 'This shift is not accepting signups', 'NOT_OPEN');
    }

    if (shift.date < new Date(new Date().toISOString().split('T')[0])) {
      throw new AppError(400, 'This shift has already passed', 'SHIFT_PAST');
    }

    // Pre-check capacity (definitive check is inside transaction below)
    if (shift.currentVolunteers >= shift.maxVolunteers) {
      throw new AppError(400, 'Shift is full', 'SHIFT_FULL');
    }

    // Check unique constraint
    const existingSignup = await prisma.shiftSignup.findUnique({
      where: { shiftId_userEmail: { shiftId, userEmail: data.email } },
    });

    if (existingSignup && existingSignup.status === SignupStatus.CONFIRMED) {
      throw new AppError(409, 'You are already signed up for this shift', 'DUPLICATE_SIGNUP');
    }

    // Look up existing user
    let user = await prisma.user.findUnique({ where: { email: data.email } });
    let isNewUser = false;
    let tempPassword: string | undefined;

    if (!user) {
      // Create temp user
      tempPassword = generateReadablePassword();
      const hashedPassword = await bcrypt.hash(tempPassword, 12);

      const shiftDate = new Date(shift.date);
      shiftDate.setDate(shiftDate.getDate() + 1);

      user = await prisma.user.create({
        data: {
          email: data.email,
          password: hashedPassword,
          name: data.name,
          phone: data.phone,
          role: 'TEMP',
          roles: JSON.parse(JSON.stringify(['TEMP'])),
          createdVia: 'PUBLIC_SHIFT_SIGNUP',
          expiresAt: shiftDate,
        },
      });
      isNewUser = true;
    }

    // Atomic signup + capacity check inside transaction to prevent TOCTOU race
    const signup = await prisma.$transaction(async (tx) => {
      // Re-check capacity atomically inside the transaction
      const currentShift = await tx.shift.findUnique({ where: { id: shiftId } });
      if (!currentShift || currentShift.currentVolunteers >= currentShift.maxVolunteers) {
        throw new AppError(400, 'Shift is full', 'SHIFT_FULL');
      }

      let created;
      if (existingSignup && existingSignup.status === SignupStatus.CANCELLED) {
        created = await tx.shiftSignup.update({
          where: { id: existingSignup.id },
          data: {
            status: SignupStatus.CONFIRMED,
            signupSource: user ? SignupSource.AUTHENTICATED : SignupSource.PUBLIC,
            userName: data.name,
            userPhone: data.phone,
            userId: user!.id,
          },
        });
      } else {
        created = await tx.shiftSignup.create({
          data: {
            shiftId,
            shiftTitle: currentShift.title,
            userId: user!.id,
            userEmail: data.email,
            userName: data.name,
            userPhone: data.phone,
            signupSource: isNewUser ? SignupSource.PUBLIC : SignupSource.AUTHENTICATED,
          },
        });
      }

      await tx.shift.update({
        where: { id: shiftId },
        data: {
          currentVolunteers: { increment: 1 },
          status: currentShift.currentVolunteers + 1 >= currentShift.maxVolunteers ? ShiftStatus.FULL : undefined,
        },
      });

      return created;
    });

    // Send confirmation email
    try {
      const shiftDate = new Date(shift.date);
      const dateStr = shiftDate.toLocaleDateString('en-CA', {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      });

      await emailService.sendShiftSignupConfirmation({
        recipientEmail: data.email,
        recipientName: data.name,
        shiftTitle: shift.title,
        shiftDate: dateStr,
        shiftTime: `${shift.startTime} — ${shift.endTime}`,
        shiftLocation: shift.location || 'TBD',
        isNewUser,
        tempPassword,
        loginUrl: `${env.CORS_ORIGINS.split(',')[0].trim()}/login`,
      });
    } catch (err) {
      logger.error('Failed to send shift signup confirmation email:', err);
    }

    // SMS signup confirmation (fire-and-forget)
    if (data.phone) {
      const shiftDate = new Date(shift.date);
      const smsDateStr = shiftDate.toLocaleDateString('en-CA', {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      });
      smsNotificationService.sendShiftSignupConfirmation(data.phone, {
        name: data.name,
        shiftTitle: shift.title,
        shiftDate: smsDateStr,
        shiftTime: `${shift.startTime} — ${shift.endTime}`,
      }).catch(err => logger.error('SMS signup confirmation failed:', err));
    }

    // Notification: admin shift signup alert
    try {
      if (await isNotificationEnabled('notifyAdminShiftSignup')) {
        const adminEmails = await getAdminEmailsByRole([UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN]);
        if (adminEmails.length > 0) {
          const adminUrl = `${env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
          const shiftDate = new Date(shift.date);
          const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
          await notificationQueueService.enqueue({
            type: 'admin-shift-signup',
            adminEmails,
            shiftTitle: shift.title,
            shiftDate: dateStr,
            volunteerName: data.name,
            volunteerEmail: data.email,
            signupSource: 'Public Form',
            adminUrl,
          });
        }
      }
    } catch (err) {
      logger.error('Failed to enqueue admin shift signup notification:', err);
    }

    // Notification: schedule 24h pre-shift reminder
    try {
      if (await isNotificationEnabled('notifyVolunteerShiftReminder')) {
        const shiftDatetime = new Date(shift.date);
        const [startH, startM] = shift.startTime.split(':').map(Number);
        shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);

        await notificationQueueService.scheduleShiftReminder({
          type: 'volunteer-shift-reminder',
          recipientEmail: data.email,
          recipientName: data.name,
          shiftTitle: shift.title,
          shiftDate: shiftDatetime.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
          shiftStartTime: shift.startTime,
          shiftEndTime: shift.endTime,
          shiftLocation: shift.location || 'TBD',
          shiftDescription: shift.description || '',
          currentVolunteers: shift.currentVolunteers + 1,
          maxVolunteers: shift.maxVolunteers,
          shiftStatus: shift.status,
        }, shiftDatetime);
      }
    } catch (err) {
      logger.error('Failed to schedule shift reminder:', err);
    }

    // SMS shift reminder (fire-and-forget, delay calculated by notification service)
    if (data.phone) {
      const smsShiftDatetime = new Date(shift.date);
      const [smsH, smsM] = shift.startTime.split(':').map(Number);
      smsShiftDatetime.setHours(smsH || 0, smsM || 0, 0, 0);

      smsNotificationService.sendShiftReminder(data.phone, {
        name: data.name,
        shiftTitle: shift.title,
        shiftTime: shift.startTime,
        shiftLocation: shift.location || 'TBD',
      }, smsShiftDatetime).catch(err => logger.error('SMS shift reminder failed:', err));
    }

    // Notification: schedule post-shift thank-you (2h after end)
    try {
      if (await isNotificationEnabled('notifyVolunteerShiftThankYou')) {
        const shiftEndDatetime = new Date(shift.date);
        const [endH, endM] = shift.endTime.split(':').map(Number);
        shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);

        const shiftDate = new Date(shift.date);
        const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
        const signupUrl = `${env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;

        await notificationQueueService.scheduleShiftThankYou({
          type: 'volunteer-shift-thank-you',
          volunteerEmail: data.email,
          volunteerName: data.name,
          shiftTitle: shift.title,
          shiftDate: dateStr,
          shiftTime: `${shift.startTime} — ${shift.endTime}`,
          shiftLocation: shift.location || 'TBD',
          signupUrl,
        }, shiftEndDatetime);
      }
    } catch (err) {
      logger.error('Failed to schedule shift thank-you:', err);
    }

    recordShiftSignup();

    // Publish shift.signup.created event (listeners: Listmonk, RC, CRM, n8n)
    eventBus.publish('shift.signup.created', {
      shiftId,
      shiftTitle: shift.title,
      shiftDate: new Date(shift.date).toISOString().split('T')[0],
      userName: data.name || data.email,
      userEmail: data.email,
      userId: user?.id ?? null,
      cutName: null,
      signupType: 'public',
    });

    // Social group sync (fire-and-forget)
    groupService.syncShiftTeam(shiftId).catch(() => {});

    // Achievement check (fire-and-forget)
    if (user?.id) achievementsService.checkAndUnlock(user.id, ['shifts']).catch(() => {});

    return { signup, isNewUser };
  },

  async cancelPublicSignup(shiftId: string, userEmail: string) {
    const signup = await prisma.shiftSignup.findUnique({
      where: { shiftId_userEmail: { shiftId, userEmail } },
    });

    if (!signup) {
      throw new AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
    }

    if (signup.status === SignupStatus.CANCELLED) {
      throw new AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
    }

    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });

    await prisma.$transaction([
      prisma.shiftSignup.update({
        where: { id: signup.id },
        data: { status: SignupStatus.CANCELLED },
      }),
      prisma.shift.update({
        where: { id: shiftId },
        data: {
          currentVolunteers: { decrement: 1 },
          status: ShiftStatus.OPEN,
        },
      }),
    ]);

    // Notification: cancellation acknowledgement + cancel reminder
    try {
      if (shift && await isNotificationEnabled('notifyVolunteerCancellation')) {
        const shiftDate = new Date(shift.date);
        const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
        const signupUrl = `${env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
        await notificationQueueService.enqueue({
          type: 'volunteer-cancellation',
          volunteerEmail: userEmail,
          volunteerName: signup.userName || userEmail,
          shiftTitle: shift.title,
          shiftDate: dateStr,
          shiftTime: `${shift.startTime} — ${shift.endTime}`,
          signupUrl,
        });
      }

      // Cancel the pending shift reminder
      if (shift) {
        const shiftDatetime = new Date(shift.date);
        const [startH, startM] = shift.startTime.split(':').map(Number);
        shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);
        await notificationQueueService.cancelShiftReminder(userEmail, shiftDatetime);
      }

      // Cancel the pending shift thank-you
      if (shift) {
        const shiftEndDatetime = new Date(shift.date);
        const [endH, endM] = shift.endTime.split(':').map(Number);
        shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);
        await notificationQueueService.cancelShiftThankYou(userEmail, shiftEndDatetime);
      }
    } catch (err) {
      logger.error('Failed to enqueue cancellation notification:', err);
    }

    // Publish shift.signup.cancelled event (listeners: RC, n8n)
    if (shift) {
      eventBus.publish('shift.signup.cancelled', {
        shiftId,
        shiftTitle: shift.title,
        shiftDate: new Date(shift.date).toISOString().split('T')[0],
        userName: signup.userName || userEmail,
        userEmail,
        signupType: 'public',
      });
    }

    // Notification: admin shift cancellation alert
    try {
      if (shift && await isNotificationEnabled('notifyAdminShiftCancellation')) {
        const adminEmails = await getAdminEmailsByRole([UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN]);
        if (adminEmails.length > 0) {
          const adminUrl = `${env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
          const shiftDate = new Date(shift.date);
          const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
          await notificationQueueService.enqueue({
            type: 'admin-shift-cancellation',
            adminEmails,
            shiftTitle: shift.title,
            shiftDate: dateStr,
            volunteerName: signup.userName || userEmail,
            volunteerEmail: userEmail,
            cancellationSource: 'Public Form',
            adminUrl,
          });
        }
      }
    } catch (err) {
      logger.error('Failed to enqueue admin shift cancellation notification:', err);
    }

    // Social group sync (fire-and-forget)
    groupService.syncShiftTeam(shiftId).catch(() => {});
  },

  async getUpcomingForVolunteer(userId: string) {
    const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
    if (!user) throw new AppError(404, 'User not found', 'USER_NOT_FOUND');

    const shifts = await prisma.shift.findMany({
      where: {
        isPublic: true,
        status: { not: ShiftStatus.CANCELLED },
        date: { gte: new Date(new Date().toISOString().split('T')[0]) },
      },
      select: {
        id: true,
        title: true,
        description: true,
        date: true,
        startTime: true,
        endTime: true,
        location: true,
        maxVolunteers: true,
        currentVolunteers: true,
        status: true,
        meeting: { select: { id: true, slug: true, isActive: true } },
      },
      orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
    });

    // Check signup status for each shift
    const signups = await prisma.shiftSignup.findMany({
      where: {
        userEmail: user.email,
        shiftId: { in: shifts.map((s) => s.id) },
        status: SignupStatus.CONFIRMED,
      },
      select: { shiftId: true },
    });

    const signedUpSet = new Set(signups.map((s) => s.shiftId));

    return shifts.map((s) => ({
      ...s,
      isSignedUp: signedUpSet.has(s.id),
    }));
  },

  async volunteerSignup(shiftId: string, userId: string) {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, email: true, name: true, phone: true },
    });
    if (!user) throw new AppError(404, 'User not found', 'USER_NOT_FOUND');

    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });
    if (!shift) throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    if (!shift.isPublic) throw new AppError(403, 'This shift is not open for signup', 'NOT_PUBLIC');
    if (shift.status === ShiftStatus.CANCELLED) throw new AppError(400, 'This shift is cancelled', 'SHIFT_CANCELLED');
    if (shift.date < new Date(new Date().toISOString().split('T')[0])) throw new AppError(400, 'This shift has already passed', 'SHIFT_PAST');
    if (shift.currentVolunteers >= shift.maxVolunteers) throw new AppError(400, 'Shift is full', 'SHIFT_FULL');

    const existing = await prisma.shiftSignup.findUnique({
      where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
    });

    if (existing && existing.status === SignupStatus.CONFIRMED) {
      throw new AppError(409, 'Already signed up for this shift', 'DUPLICATE_SIGNUP');
    }

    let signup;
    if (existing && existing.status === SignupStatus.CANCELLED) {
      [signup] = await prisma.$transaction([
        prisma.shiftSignup.update({
          where: { id: existing.id },
          data: { status: SignupStatus.CONFIRMED, signupSource: SignupSource.AUTHENTICATED },
        }),
        prisma.shift.update({
          where: { id: shiftId },
          data: {
            currentVolunteers: { increment: 1 },
            status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? ShiftStatus.FULL : undefined,
          },
        }),
      ]);
    } else {
      [signup] = await prisma.$transaction([
        prisma.shiftSignup.create({
          data: {
            shiftId,
            shiftTitle: shift.title,
            userId: user.id,
            userEmail: user.email,
            userName: user.name,
            userPhone: user.phone,
            signupSource: SignupSource.AUTHENTICATED,
          },
        }),
        prisma.shift.update({
          where: { id: shiftId },
          data: {
            currentVolunteers: { increment: 1 },
            status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? ShiftStatus.FULL : undefined,
          },
        }),
      ]);
    }

    // Send confirmation email
    try {
      const shiftDate = new Date(shift.date);
      const dateStr = shiftDate.toLocaleDateString('en-CA', {
        weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
      });

      await emailService.sendShiftSignupConfirmation({
        recipientEmail: user.email,
        recipientName: user.name || user.email,
        shiftTitle: shift.title,
        shiftDate: dateStr,
        shiftTime: `${shift.startTime} — ${shift.endTime}`,
        shiftLocation: shift.location || 'TBD',
        isNewUser: false,
        loginUrl: `${env.CORS_ORIGINS.split(',')[0].trim()}/login`,
      });
    } catch (err) {
      logger.error('Failed to send volunteer shift signup confirmation email:', err);
    }

    // Notification: admin shift signup alert
    try {
      if (await isNotificationEnabled('notifyAdminShiftSignup')) {
        const adminEmails = await getAdminEmailsByRole([UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN]);
        if (adminEmails.length > 0) {
          const adminUrl = `${env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
          const shiftDate = new Date(shift.date);
          const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
          await notificationQueueService.enqueue({
            type: 'admin-shift-signup',
            adminEmails,
            shiftTitle: shift.title,
            shiftDate: dateStr,
            volunteerName: user.name || user.email,
            volunteerEmail: user.email,
            signupSource: 'Authenticated Volunteer',
            adminUrl,
          });
        }
      }
    } catch (err) {
      logger.error('Failed to enqueue admin shift signup notification:', err);
    }

    // Notification: schedule 24h pre-shift reminder
    try {
      if (await isNotificationEnabled('notifyVolunteerShiftReminder')) {
        const shiftDatetime = new Date(shift.date);
        const [startH, startM] = shift.startTime.split(':').map(Number);
        shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);

        await notificationQueueService.scheduleShiftReminder({
          type: 'volunteer-shift-reminder',
          recipientEmail: user.email,
          recipientName: user.name || user.email,
          shiftTitle: shift.title,
          shiftDate: shiftDatetime.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
          shiftStartTime: shift.startTime,
          shiftEndTime: shift.endTime,
          shiftLocation: shift.location || 'TBD',
          shiftDescription: shift.description || '',
          currentVolunteers: shift.currentVolunteers + 1,
          maxVolunteers: shift.maxVolunteers,
          shiftStatus: shift.status,
        }, shiftDatetime);
      }
    } catch (err) {
      logger.error('Failed to schedule shift reminder:', err);
    }

    // Notification: schedule post-shift thank-you (2h after end)
    try {
      if (await isNotificationEnabled('notifyVolunteerShiftThankYou')) {
        const shiftEndDatetime = new Date(shift.date);
        const [endH, endM] = shift.endTime.split(':').map(Number);
        shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);

        const shiftDate = new Date(shift.date);
        const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
        const signupUrl = `${env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;

        await notificationQueueService.scheduleShiftThankYou({
          type: 'volunteer-shift-thank-you',
          volunteerEmail: user.email,
          volunteerName: user.name || user.email,
          shiftTitle: shift.title,
          shiftDate: dateStr,
          shiftTime: `${shift.startTime} — ${shift.endTime}`,
          shiftLocation: shift.location || 'TBD',
          signupUrl,
        }, shiftEndDatetime);
      }
    } catch (err) {
      logger.error('Failed to schedule shift thank-you:', err);
    }

    // Publish shift.signup.created event (listeners: Listmonk, RC, CRM, n8n)
    eventBus.publish('shift.signup.created', {
      shiftId,
      shiftTitle: shift.title,
      shiftDate: new Date(shift.date).toISOString().split('T')[0],
      userName: user.name || user.email,
      userEmail: user.email,
      userId,
      cutName: null,
      signupType: 'volunteer',
    });

    // Social group sync (fire-and-forget)
    groupService.syncShiftTeam(shiftId).catch(() => {});

    // Achievement check (fire-and-forget)
    achievementsService.checkAndUnlock(userId, ['shifts']).catch(() => {});

    return signup;
  },

  async cancelVolunteerSignup(shiftId: string, userId: string) {
    const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } });
    if (!user) throw new AppError(404, 'User not found', 'USER_NOT_FOUND');

    const signup = await prisma.shiftSignup.findUnique({
      where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
    });

    if (!signup) throw new AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
    if (signup.status === SignupStatus.CANCELLED) throw new AppError(400, 'Already cancelled', 'ALREADY_CANCELLED');

    const shift = await prisma.shift.findUnique({ where: { id: shiftId } });

    await prisma.$transaction([
      prisma.shiftSignup.update({
        where: { id: signup.id },
        data: { status: SignupStatus.CANCELLED },
      }),
      prisma.shift.update({
        where: { id: shiftId },
        data: {
          currentVolunteers: { decrement: 1 },
          status: ShiftStatus.OPEN,
        },
      }),
    ]);

    // Notification: cancellation acknowledgement + cancel reminder
    try {
      if (shift && await isNotificationEnabled('notifyVolunteerCancellation')) {
        const shiftDate = new Date(shift.date);
        const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
        const signupUrl = `${env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
        await notificationQueueService.enqueue({
          type: 'volunteer-cancellation',
          volunteerEmail: user.email,
          volunteerName: user.name || user.email,
          shiftTitle: shift.title,
          shiftDate: dateStr,
          shiftTime: `${shift.startTime} — ${shift.endTime}`,
          signupUrl,
        });
      }

      // Cancel the pending shift reminder
      if (shift) {
        const shiftDatetime = new Date(shift.date);
        const [startH, startM] = shift.startTime.split(':').map(Number);
        shiftDatetime.setHours(startH || 0, startM || 0, 0, 0);
        await notificationQueueService.cancelShiftReminder(user.email, shiftDatetime);
      }

      // Cancel the pending shift thank-you
      if (shift) {
        const shiftEndDatetime = new Date(shift.date);
        const [endH, endM] = shift.endTime.split(':').map(Number);
        shiftEndDatetime.setHours(endH || 0, endM || 0, 0, 0);
        await notificationQueueService.cancelShiftThankYou(user.email, shiftEndDatetime);
      }
    } catch (err) {
      logger.error('Failed to enqueue cancellation notification:', err);
    }

    // Publish shift.signup.cancelled event (listeners: RC, n8n)
    if (shift) {
      eventBus.publish('shift.signup.cancelled', {
        shiftId,
        shiftTitle: shift.title,
        shiftDate: new Date(shift.date).toISOString().split('T')[0],
        userName: user.name || user.email,
        userEmail: user.email,
        signupType: 'volunteer',
      });
    }

    // Notification: admin shift cancellation alert
    try {
      if (shift && await isNotificationEnabled('notifyAdminShiftCancellation')) {
        const adminEmails = await getAdminEmailsByRole([UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN]);
        if (adminEmails.length > 0) {
          const adminUrl = `${env.ADMIN_URL || 'http://localhost:3000'}/app/map/shifts`;
          const shiftDate = new Date(shift.date);
          const dateStr = shiftDate.toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
          await notificationQueueService.enqueue({
            type: 'admin-shift-cancellation',
            adminEmails,
            shiftTitle: shift.title,
            shiftDate: dateStr,
            volunteerName: user.name || user.email,
            volunteerEmail: user.email,
            cancellationSource: 'Volunteer Portal',
            adminUrl,
          });
        }
      }
    } catch (err) {
      logger.error('Failed to enqueue admin shift cancellation notification:', err);
    }

    // Social group sync (fire-and-forget)
    groupService.syncShiftTeam(shiftId).catch(() => {});
  },

  async getMySignups(userId: string) {
    const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
    if (!user) throw new AppError(404, 'User not found', 'USER_NOT_FOUND');

    const signups = await prisma.shiftSignup.findMany({
      where: {
        userEmail: user.email,
        status: SignupStatus.CONFIRMED,
        shift: {
          date: { gte: new Date(new Date().toISOString().split('T')[0]) },
          status: { not: ShiftStatus.CANCELLED },
        },
      },
      include: {
        shift: {
          select: {
            id: true,
            title: true,
            description: true,
            date: true,
            startTime: true,
            endTime: true,
            location: true,
            maxVolunteers: true,
            currentVolunteers: true,
            status: true,
            meeting: { select: { id: true, slug: true, isActive: true } },
          },
        },
      },
      orderBy: { shift: { date: 'asc' } },
    });

    return signups;
  },

  async getPublicShifts(page: number = 1, limit: number = 20) {
    const where = {
      isPublic: true,
      status: { not: ShiftStatus.CANCELLED },
      date: { gte: new Date(new Date().toISOString().split('T')[0]) },
    };
    const skip = (page - 1) * limit;

    const [shifts, total] = await Promise.all([
      prisma.shift.findMany({
        where,
        select: {
          id: true,
          title: true,
          description: true,
          date: true,
          startTime: true,
          endTime: true,
          location: true,
          maxVolunteers: true,
          currentVolunteers: true,
          status: true,
          meeting: { select: { id: true, slug: true, isActive: true } },
        },
        orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
        skip,
        take: limit,
      }),
      prisma.shift.count({ where: { isPublic: true, status: { not: ShiftStatus.CANCELLED }, date: { gte: new Date(new Date().toISOString().split('T')[0]) } } }),
    ]);

    return {
      shifts,
      pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
    };
  },

  async emailShiftDetails(shiftId: string) {
    const shift = await prisma.shift.findUnique({
      where: { id: shiftId },
      include: {
        signups: {
          where: { status: SignupStatus.CONFIRMED },
        },
      },
    });

    if (!shift) {
      throw new AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
    }

    const shiftDate = new Date(shift.date);
    const dateStr = shiftDate.toLocaleDateString('en-CA', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    });

    let sent = 0;
    let failed = 0;

    for (const signup of shift.signups) {
      try {
        const result = await emailService.sendShiftDetailsEmail({
          recipientEmail: signup.userEmail,
          recipientName: signup.userName || signup.userEmail,
          shiftTitle: shift.title,
          shiftDate: dateStr,
          shiftStartTime: shift.startTime,
          shiftEndTime: shift.endTime,
          shiftLocation: shift.location || 'TBD',
          shiftDescription: shift.description || '',
          currentVolunteers: shift.currentVolunteers,
          maxVolunteers: shift.maxVolunteers,
          shiftStatus: shift.status,
        });

        if (result.success) {
          sent++;
        } else {
          failed++;
        }
      } catch (err) {
        logger.error(`Failed to send shift details to ${signup.userEmail}:`, err);
        failed++;
      }
    }

    return { sent, failed };
  },

  async getCalendarData(startDate: string, endDate: string) {
    const shifts = await prisma.shift.findMany({
      where: {
        date: {
          gte: new Date(startDate),
          lte: new Date(endDate),
        },
      },
      include: {
        cut: { select: { id: true, name: true } },
        signups: true,
        series: { select: { id: true, frequency: true } },
      },
      orderBy: { startTime: 'asc' },
    });

    // Group by date
    const dateMap: Record<string, { count: number; shifts: any[] }> = {};

    for (const shift of shifts) {
      const dateKey = shift.date.toISOString().split('T')[0];

      if (!dateMap[dateKey]) {
        dateMap[dateKey] = { count: 0, shifts: [] };
      }

      dateMap[dateKey].count++;
      dateMap[dateKey].shifts.push(shift);
    }

    return { dates: dateMap };
  },
};
