"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.shiftsService = void 0;
const bcryptjs_1 = __importDefault(require("bcryptjs"));
const client_1 = require("@prisma/client");
const database_1 = require("../../../config/database");
const error_handler_1 = require("../../../middleware/error-handler");
const email_service_1 = require("../../../services/email.service");
const notification_queue_service_1 = require("../../../services/notification-queue.service");
const notification_helper_1 = require("../../../services/notification.helper");
const env_1 = require("../../../config/env");
const logger_1 = require("../../../utils/logger");
const metrics_1 = require("../../../utils/metrics");
const rocketchat_webhook_service_1 = require("../../../services/rocketchat-webhook.service");
const listmonk_event_sync_service_1 = require("../../../services/listmonk-event-sync.service");
const gancio_client_1 = require("../../../services/gancio.client");
const unified_calendar_service_1 = require("../../events/unified-calendar.service");
const group_service_1 = require("../../social/group.service");
const achievements_service_1 = require("../../social/achievements.service");
const slug_1 = require("../../../utils/slug");
const settings_service_1 = require("../../settings/settings.service");
const sms_notification_service_1 = require("../../../services/sms-notification.service");
const crypto_1 = __importDefault(require("crypto"));
const adjectives = ['Blue', 'Red', 'Green', 'Swift', 'Bright', 'Bold', 'Calm', 'Fair'];
const nouns = ['Eagle', 'River', 'Mountain', 'Star', 'Forest', 'Lake', 'Wolf', 'Hawk'];
function generateReadablePassword() {
    const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
    const noun = nouns[Math.floor(Math.random() * nouns.length)];
    const num = Math.floor(Math.random() * 90) + 10;
    return `${adj}${noun}${num}`;
}
const meetingSelect = {
    id: true,
    slug: true,
    title: true,
    isActive: true,
    jitsiRoom: true,
};
exports.shiftsService = {
    async findAll(filters) {
        const { page, limit, search, status, upcoming, sortBy, sortOrder } = filters;
        const skip = (page - 1) * limit;
        const where = {};
        if (search) {
            where.OR = [
                { title: { contains: search, mode: 'insensitive' } },
                { location: { contains: search, mode: 'insensitive' } },
            ];
        }
        if (status)
            where.status = status;
        if (upcoming) {
            where.date = { gte: new Date() };
        }
        const orderBy = sortBy === 'title'
            ? { title: sortOrder }
            : sortBy === 'createdAt'
                ? { createdAt: sortOrder }
                : { date: sortOrder };
        const [shifts, total] = await Promise.all([
            database_1.prisma.shift.findMany({
                where,
                skip,
                take: limit,
                orderBy,
                include: {
                    cut: { select: { id: true, name: true } },
                    meeting: { select: meetingSelect },
                    _count: {
                        select: {
                            signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
                        },
                    },
                },
            }),
            database_1.prisma.shift.count({ where }),
        ]);
        return {
            shifts,
            pagination: {
                page,
                limit,
                total,
                totalPages: Math.ceil(total / limit),
            },
        };
    },
    async findById(id) {
        const shift = await database_1.prisma.shift.findUnique({
            where: { id },
            include: {
                cut: { select: { id: true, name: true } },
                meeting: { select: meetingSelect },
                signups: {
                    where: { status: client_1.SignupStatus.CONFIRMED },
                    include: { user: { select: { id: true, email: true, name: true, phone: true } } },
                    orderBy: { signupDate: 'desc' },
                },
                _count: {
                    select: {
                        signups: { where: { status: client_1.SignupStatus.CONFIRMED } },
                    },
                },
            },
        });
        if (!shift) {
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        }
        return shift;
    },
    async create(data, userId) {
        const shift = await database_1.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,
                createdBy: userId,
            },
        });
        // Gancio event sync (fire-and-forget)
        if (gancio_client_1.gancioClient.enabled) {
            gancio_client_1.gancioClient.createEvent({
                title: shift.title,
                description: shift.description,
                location: shift.location,
                date: shift.date,
                startTime: shift.startTime,
                endTime: shift.endTime,
            }).then(async (eventId) => {
                if (eventId) {
                    await database_1.prisma.shift.update({
                        where: { id: shift.id },
                        data: { gancioEventId: eventId },
                    });
                }
            }).catch((err) => {
                logger_1.logger.warn('Gancio sync on shift create failed:', err);
            });
        }
        // Bust unified calendar cache
        unified_calendar_service_1.unifiedCalendarService.bustCache().catch(() => { });
        return shift;
    },
    async update(id, data) {
        const existing = await database_1.prisma.shift.findUnique({ where: { id } });
        if (!existing) {
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        }
        const updateData = { ...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 === client_1.ShiftStatus.OPEN) {
                updateData.status = client_1.ShiftStatus.FULL;
            }
            else if (existing.currentVolunteers < data.maxVolunteers && existing.status === client_1.ShiftStatus.FULL) {
                updateData.status = client_1.ShiftStatus.OPEN;
            }
        }
        const shift = await database_1.prisma.shift.update({
            where: { id },
            data: updateData,
        });
        // Gancio event sync (fire-and-forget)
        if (gancio_client_1.gancioClient.enabled && shift.gancioEventId) {
            gancio_client_1.gancioClient.updateEvent(shift.gancioEventId, {
                title: shift.title,
                description: shift.description,
                location: shift.location,
                date: shift.date,
                startTime: shift.startTime,
                endTime: shift.endTime,
            }).catch((err) => {
                logger_1.logger.warn('Gancio sync on shift update failed:', err);
            });
        }
        // Bust unified calendar cache
        unified_calendar_service_1.unifiedCalendarService.bustCache().catch(() => { });
        return shift;
    },
    async delete(id) {
        const existing = await database_1.prisma.shift.findUnique({ where: { id } });
        if (!existing) {
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        }
        // Delete Gancio event before deleting shift (fire-and-forget)
        if (gancio_client_1.gancioClient.enabled && existing.gancioEventId) {
            gancio_client_1.gancioClient.deleteEvent(existing.gancioEventId).catch((err) => {
                logger_1.logger.warn('Gancio sync on shift delete failed:', err);
            });
        }
        // Delete associated meeting if exists
        if (existing.meetingId) {
            await database_1.prisma.meeting.delete({ where: { id: existing.meetingId } }).catch(() => { });
        }
        await database_1.prisma.shift.delete({ where: { id } });
        // Bust unified calendar cache
        unified_calendar_service_1.unifiedCalendarService.bustCache().catch(() => { });
    },
    async createMeetingForShift(shiftId, userId) {
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        if (!shift)
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        if (shift.meetingId)
            throw new error_handler_1.AppError(400, 'Shift already has a meeting', 'MEETING_EXISTS');
        const settings = await settings_service_1.siteSettingsService.get();
        if (!settings.enableMeet)
            throw new error_handler_1.AppError(400, 'Video meetings are not enabled', 'MEET_DISABLED');
        const meeting = await database_1.prisma.meeting.create({
            data: {
                slug: (0, slug_1.generateSlug)(shift.title),
                title: `${shift.title} — Video Briefing`,
                jitsiRoom: crypto_1.default.randomUUID(),
                createdByUserId: userId,
            },
        });
        await database_1.prisma.shift.update({
            where: { id: shiftId },
            data: { meetingId: meeting.id },
        });
        return meeting;
    },
    async removeMeetingFromShift(shiftId) {
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        if (!shift)
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        if (!shift.meetingId)
            throw new error_handler_1.AppError(400, 'Shift has no meeting', 'NO_MEETING');
        const meetingId = shift.meetingId;
        await database_1.prisma.shift.update({
            where: { id: shiftId },
            data: { meetingId: null },
        });
        // Delete the meeting record
        await database_1.prisma.meeting.delete({ where: { id: meetingId } }).catch(() => { });
    },
    async getStats() {
        const [total, open, full, cancelled, upcoming, totalSignups] = await Promise.all([
            database_1.prisma.shift.count(),
            database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.OPEN } }),
            database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.FULL } }),
            database_1.prisma.shift.count({ where: { status: client_1.ShiftStatus.CANCELLED } }),
            database_1.prisma.shift.count({ where: { date: { gte: new Date() }, status: { not: client_1.ShiftStatus.CANCELLED } } }),
            database_1.prisma.shiftSignup.count({ where: { status: client_1.SignupStatus.CONFIRMED } }),
        ]);
        return { total, open, full, cancelled, upcoming, totalSignups };
    },
    async getSignups(shiftId) {
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        if (!shift) {
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        }
        return database_1.prisma.shiftSignup.findMany({
            where: { shiftId },
            include: { user: { select: { id: true, email: true, name: true, phone: true } } },
            orderBy: { signupDate: 'desc' },
        });
    },
    async addSignup(shiftId, data) {
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        if (!shift) {
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        }
        if (shift.currentVolunteers >= shift.maxVolunteers) {
            throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
        }
        // Check unique constraint
        const existing = await database_1.prisma.shiftSignup.findUnique({
            where: { shiftId_userEmail: { shiftId, userEmail: data.userEmail } },
        });
        if (existing) {
            if (existing.status === client_1.SignupStatus.CANCELLED) {
                // Re-activate cancelled signup
                const [signup] = await database_1.prisma.$transaction([
                    database_1.prisma.shiftSignup.update({
                        where: { id: existing.id },
                        data: { status: client_1.SignupStatus.CONFIRMED, signupSource: client_1.SignupSource.ADMIN },
                    }),
                    database_1.prisma.shift.update({
                        where: { id: shiftId },
                        data: {
                            currentVolunteers: { increment: 1 },
                            status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
                        },
                    }),
                ]);
                return signup;
            }
            throw new error_handler_1.AppError(409, 'Volunteer already signed up', 'DUPLICATE_SIGNUP');
        }
        // Look up user
        const user = await database_1.prisma.user.findUnique({ where: { email: data.userEmail } });
        const [signup] = await database_1.prisma.$transaction([
            database_1.prisma.shiftSignup.create({
                data: {
                    shiftId,
                    shiftTitle: shift.title,
                    userId: user?.id,
                    userEmail: data.userEmail,
                    userName: data.userName || user?.name,
                    signupSource: client_1.SignupSource.ADMIN,
                },
            }),
            database_1.prisma.shift.update({
                where: { id: shiftId },
                data: {
                    currentVolunteers: { increment: 1 },
                    status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
                },
            }),
        ]);
        // Listmonk event sync
        listmonk_event_sync_service_1.listmonkEventSyncService.onShiftSignup({
            email: data.userEmail,
            name: data.userName || data.userEmail,
            shiftTitle: shift.title,
            shiftDate: new Date(shift.date).toISOString().split('T')[0],
        }).catch(() => { });
        // Social group sync (fire-and-forget)
        group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
        // Achievement check (fire-and-forget)
        if (user?.id)
            achievements_service_1.achievementsService.checkAndUnlock(user.id, ['shifts']).catch(() => { });
        return signup;
    },
    async removeSignup(signupId) {
        const signup = await database_1.prisma.shiftSignup.findUnique({ where: { id: signupId } });
        if (!signup) {
            throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
        }
        if (signup.status === client_1.SignupStatus.CANCELLED) {
            throw new error_handler_1.AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
        }
        await database_1.prisma.$transaction([
            database_1.prisma.shiftSignup.update({
                where: { id: signupId },
                data: { status: client_1.SignupStatus.CANCELLED },
            }),
            database_1.prisma.shift.update({
                where: { id: signup.shiftId },
                data: {
                    currentVolunteers: { decrement: 1 },
                    status: client_1.ShiftStatus.OPEN,
                },
            }),
        ]);
        // Social group sync (fire-and-forget)
        group_service_1.groupService.syncShiftTeam(signup.shiftId).catch(() => { });
    },
    async publicSignup(shiftId, data) {
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        if (!shift) {
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        }
        if (!shift.isPublic) {
            throw new error_handler_1.AppError(403, 'This shift is not open for public signup', 'NOT_PUBLIC');
        }
        if (shift.status !== client_1.ShiftStatus.OPEN) {
            throw new error_handler_1.AppError(400, 'This shift is not accepting signups', 'NOT_OPEN');
        }
        if (shift.date < new Date(new Date().toISOString().split('T')[0])) {
            throw new error_handler_1.AppError(400, 'This shift has already passed', 'SHIFT_PAST');
        }
        if (shift.currentVolunteers >= shift.maxVolunteers) {
            throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
        }
        // Check unique constraint
        const existingSignup = await database_1.prisma.shiftSignup.findUnique({
            where: { shiftId_userEmail: { shiftId, userEmail: data.email } },
        });
        if (existingSignup && existingSignup.status === client_1.SignupStatus.CONFIRMED) {
            throw new error_handler_1.AppError(409, 'You are already signed up for this shift', 'DUPLICATE_SIGNUP');
        }
        // Look up existing user
        let user = await database_1.prisma.user.findUnique({ where: { email: data.email } });
        let isNewUser = false;
        let tempPassword;
        if (!user) {
            // Create temp user
            tempPassword = generateReadablePassword();
            const hashedPassword = await bcryptjs_1.default.hash(tempPassword, 12);
            const shiftDate = new Date(shift.date);
            shiftDate.setDate(shiftDate.getDate() + 1);
            user = await database_1.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;
        }
        // Create signup (or re-activate cancelled one)
        let signup;
        if (existingSignup && existingSignup.status === client_1.SignupStatus.CANCELLED) {
            [signup] = await database_1.prisma.$transaction([
                database_1.prisma.shiftSignup.update({
                    where: { id: existingSignup.id },
                    data: {
                        status: client_1.SignupStatus.CONFIRMED,
                        signupSource: user ? client_1.SignupSource.AUTHENTICATED : client_1.SignupSource.PUBLIC,
                        userName: data.name,
                        userPhone: data.phone,
                        userId: user.id,
                    },
                }),
                database_1.prisma.shift.update({
                    where: { id: shiftId },
                    data: {
                        currentVolunteers: { increment: 1 },
                        status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
                    },
                }),
            ]);
        }
        else {
            [signup] = await database_1.prisma.$transaction([
                database_1.prisma.shiftSignup.create({
                    data: {
                        shiftId,
                        shiftTitle: shift.title,
                        userId: user.id,
                        userEmail: data.email,
                        userName: data.name,
                        userPhone: data.phone,
                        signupSource: isNewUser ? client_1.SignupSource.PUBLIC : client_1.SignupSource.AUTHENTICATED,
                    },
                }),
                database_1.prisma.shift.update({
                    where: { id: shiftId },
                    data: {
                        currentVolunteers: { increment: 1 },
                        status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.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 email_service_1.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_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
            });
        }
        catch (err) {
            logger_1.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',
            });
            sms_notification_service_1.smsNotificationService.sendShiftSignupConfirmation(data.phone, {
                name: data.name,
                shiftTitle: shift.title,
                shiftDate: smsDateStr,
                shiftTime: `${shift.startTime} — ${shift.endTime}`,
            }).catch(err => logger_1.logger.error('SMS signup confirmation failed:', err));
        }
        // Notify Rocket.Chat
        const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
        rocketchat_webhook_service_1.rocketchatWebhookService.onShiftSignup({
            userName: data.name || data.email,
            shiftTitle: shift.title,
            shiftDate: shiftDateStr,
        }).catch(() => { });
        // Notification: admin shift signup alert
        try {
            if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftSignup')) {
                const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
                if (adminEmails.length > 0) {
                    const adminUrl = `${env_1.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 notification_queue_service_1.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_1.logger.error('Failed to enqueue admin shift signup notification:', err);
        }
        // Notification: schedule 24h pre-shift reminder
        try {
            if (await (0, notification_helper_1.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 notification_queue_service_1.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_1.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);
            sms_notification_service_1.smsNotificationService.sendShiftReminder(data.phone, {
                name: data.name,
                shiftTitle: shift.title,
                shiftTime: shift.startTime,
                shiftLocation: shift.location || 'TBD',
            }, smsShiftDatetime).catch(err => logger_1.logger.error('SMS shift reminder failed:', err));
        }
        // Notification: schedule post-shift thank-you (2h after end)
        try {
            if (await (0, notification_helper_1.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_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
                await notification_queue_service_1.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_1.logger.error('Failed to schedule shift thank-you:', err);
        }
        (0, metrics_1.recordShiftSignup)();
        // Listmonk event sync
        listmonk_event_sync_service_1.listmonkEventSyncService.onShiftSignup({
            email: data.email,
            name: data.name,
            shiftTitle: shift.title,
            shiftDate: new Date(shift.date).toISOString().split('T')[0],
        }).catch(() => { });
        // Social group sync (fire-and-forget)
        group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
        // Achievement check (fire-and-forget)
        if (user?.id)
            achievements_service_1.achievementsService.checkAndUnlock(user.id, ['shifts']).catch(() => { });
        return { signup, isNewUser };
    },
    async cancelPublicSignup(shiftId, userEmail) {
        const signup = await database_1.prisma.shiftSignup.findUnique({
            where: { shiftId_userEmail: { shiftId, userEmail } },
        });
        if (!signup) {
            throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
        }
        if (signup.status === client_1.SignupStatus.CANCELLED) {
            throw new error_handler_1.AppError(400, 'Signup already cancelled', 'ALREADY_CANCELLED');
        }
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        await database_1.prisma.$transaction([
            database_1.prisma.shiftSignup.update({
                where: { id: signup.id },
                data: { status: client_1.SignupStatus.CANCELLED },
            }),
            database_1.prisma.shift.update({
                where: { id: shiftId },
                data: {
                    currentVolunteers: { decrement: 1 },
                    status: client_1.ShiftStatus.OPEN,
                },
            }),
        ]);
        // Notification: cancellation acknowledgement + cancel reminder
        try {
            if (shift && await (0, notification_helper_1.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_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
                await notification_queue_service_1.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 notification_queue_service_1.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 notification_queue_service_1.notificationQueueService.cancelShiftThankYou(userEmail, shiftEndDatetime);
            }
        }
        catch (err) {
            logger_1.logger.error('Failed to enqueue cancellation notification:', err);
        }
        // Notify Rocket.Chat of cancellation
        if (shift) {
            const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
            rocketchat_webhook_service_1.rocketchatWebhookService.onShiftCancellation({
                userName: signup.userName || userEmail,
                shiftTitle: shift.title,
                shiftDate: shiftDateStr,
            }).catch(() => { });
        }
        // Notification: admin shift cancellation alert
        try {
            if (shift && await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftCancellation')) {
                const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
                if (adminEmails.length > 0) {
                    const adminUrl = `${env_1.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 notification_queue_service_1.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_1.logger.error('Failed to enqueue admin shift cancellation notification:', err);
        }
        // Social group sync (fire-and-forget)
        group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
    },
    async getUpcomingForVolunteer(userId) {
        const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
        if (!user)
            throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
        const shifts = await database_1.prisma.shift.findMany({
            where: {
                isPublic: true,
                status: { not: client_1.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 database_1.prisma.shiftSignup.findMany({
            where: {
                userEmail: user.email,
                shiftId: { in: shifts.map((s) => s.id) },
                status: client_1.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, userId) {
        const user = await database_1.prisma.user.findUnique({
            where: { id: userId },
            select: { id: true, email: true, name: true, phone: true },
        });
        if (!user)
            throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        if (!shift)
            throw new error_handler_1.AppError(404, 'Shift not found', 'SHIFT_NOT_FOUND');
        if (!shift.isPublic)
            throw new error_handler_1.AppError(403, 'This shift is not open for signup', 'NOT_PUBLIC');
        if (shift.status === client_1.ShiftStatus.CANCELLED)
            throw new error_handler_1.AppError(400, 'This shift is cancelled', 'SHIFT_CANCELLED');
        if (shift.date < new Date(new Date().toISOString().split('T')[0]))
            throw new error_handler_1.AppError(400, 'This shift has already passed', 'SHIFT_PAST');
        if (shift.currentVolunteers >= shift.maxVolunteers)
            throw new error_handler_1.AppError(400, 'Shift is full', 'SHIFT_FULL');
        const existing = await database_1.prisma.shiftSignup.findUnique({
            where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
        });
        if (existing && existing.status === client_1.SignupStatus.CONFIRMED) {
            throw new error_handler_1.AppError(409, 'Already signed up for this shift', 'DUPLICATE_SIGNUP');
        }
        let signup;
        if (existing && existing.status === client_1.SignupStatus.CANCELLED) {
            [signup] = await database_1.prisma.$transaction([
                database_1.prisma.shiftSignup.update({
                    where: { id: existing.id },
                    data: { status: client_1.SignupStatus.CONFIRMED, signupSource: client_1.SignupSource.AUTHENTICATED },
                }),
                database_1.prisma.shift.update({
                    where: { id: shiftId },
                    data: {
                        currentVolunteers: { increment: 1 },
                        status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.ShiftStatus.FULL : undefined,
                    },
                }),
            ]);
        }
        else {
            [signup] = await database_1.prisma.$transaction([
                database_1.prisma.shiftSignup.create({
                    data: {
                        shiftId,
                        shiftTitle: shift.title,
                        userId: user.id,
                        userEmail: user.email,
                        userName: user.name,
                        userPhone: user.phone,
                        signupSource: client_1.SignupSource.AUTHENTICATED,
                    },
                }),
                database_1.prisma.shift.update({
                    where: { id: shiftId },
                    data: {
                        currentVolunteers: { increment: 1 },
                        status: shift.currentVolunteers + 1 >= shift.maxVolunteers ? client_1.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 email_service_1.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_1.env.CORS_ORIGINS.split(',')[0].trim()}/login`,
            });
        }
        catch (err) {
            logger_1.logger.error('Failed to send volunteer shift signup confirmation email:', err);
        }
        // Notify Rocket.Chat
        const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
        rocketchat_webhook_service_1.rocketchatWebhookService.onShiftSignup({
            userName: user.name || user.email,
            shiftTitle: shift.title,
            shiftDate: shiftDateStr,
        }).catch(() => { });
        // Notification: admin shift signup alert
        try {
            if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftSignup')) {
                const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
                if (adminEmails.length > 0) {
                    const adminUrl = `${env_1.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 notification_queue_service_1.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_1.logger.error('Failed to enqueue admin shift signup notification:', err);
        }
        // Notification: schedule 24h pre-shift reminder
        try {
            if (await (0, notification_helper_1.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 notification_queue_service_1.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_1.logger.error('Failed to schedule shift reminder:', err);
        }
        // Notification: schedule post-shift thank-you (2h after end)
        try {
            if (await (0, notification_helper_1.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_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
                await notification_queue_service_1.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_1.logger.error('Failed to schedule shift thank-you:', err);
        }
        // Listmonk event sync
        listmonk_event_sync_service_1.listmonkEventSyncService.onShiftSignup({
            email: user.email,
            name: user.name || user.email,
            shiftTitle: shift.title,
            shiftDate: new Date(shift.date).toISOString().split('T')[0],
        }).catch(() => { });
        // Social group sync (fire-and-forget)
        group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
        // Achievement check (fire-and-forget)
        achievements_service_1.achievementsService.checkAndUnlock(userId, ['shifts']).catch(() => { });
        return signup;
    },
    async cancelVolunteerSignup(shiftId, userId) {
        const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } });
        if (!user)
            throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
        const signup = await database_1.prisma.shiftSignup.findUnique({
            where: { shiftId_userEmail: { shiftId, userEmail: user.email } },
        });
        if (!signup)
            throw new error_handler_1.AppError(404, 'Signup not found', 'SIGNUP_NOT_FOUND');
        if (signup.status === client_1.SignupStatus.CANCELLED)
            throw new error_handler_1.AppError(400, 'Already cancelled', 'ALREADY_CANCELLED');
        const shift = await database_1.prisma.shift.findUnique({ where: { id: shiftId } });
        await database_1.prisma.$transaction([
            database_1.prisma.shiftSignup.update({
                where: { id: signup.id },
                data: { status: client_1.SignupStatus.CANCELLED },
            }),
            database_1.prisma.shift.update({
                where: { id: shiftId },
                data: {
                    currentVolunteers: { decrement: 1 },
                    status: client_1.ShiftStatus.OPEN,
                },
            }),
        ]);
        // Notification: cancellation acknowledgement + cancel reminder
        try {
            if (shift && await (0, notification_helper_1.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_1.env.CORS_ORIGINS.split(',')[0].trim()}/shifts`;
                await notification_queue_service_1.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 notification_queue_service_1.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 notification_queue_service_1.notificationQueueService.cancelShiftThankYou(user.email, shiftEndDatetime);
            }
        }
        catch (err) {
            logger_1.logger.error('Failed to enqueue cancellation notification:', err);
        }
        // Notify Rocket.Chat of cancellation
        if (shift) {
            const shiftDateStr = new Date(shift.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
            rocketchat_webhook_service_1.rocketchatWebhookService.onShiftCancellation({
                userName: user.name || user.email,
                shiftTitle: shift.title,
                shiftDate: shiftDateStr,
            }).catch(() => { });
        }
        // Notification: admin shift cancellation alert
        try {
            if (shift && await (0, notification_helper_1.isNotificationEnabled)('notifyAdminShiftCancellation')) {
                const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN]);
                if (adminEmails.length > 0) {
                    const adminUrl = `${env_1.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 notification_queue_service_1.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_1.logger.error('Failed to enqueue admin shift cancellation notification:', err);
        }
        // Social group sync (fire-and-forget)
        group_service_1.groupService.syncShiftTeam(shiftId).catch(() => { });
    },
    async getMySignups(userId) {
        const user = await database_1.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
        if (!user)
            throw new error_handler_1.AppError(404, 'User not found', 'USER_NOT_FOUND');
        const signups = await database_1.prisma.shiftSignup.findMany({
            where: {
                userEmail: user.email,
                status: client_1.SignupStatus.CONFIRMED,
                shift: {
                    date: { gte: new Date(new Date().toISOString().split('T')[0]) },
                    status: { not: client_1.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() {
        const shifts = await database_1.prisma.shift.findMany({
            where: {
                isPublic: true,
                status: { not: client_1.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' }],
        });
        return shifts;
    },
    async emailShiftDetails(shiftId) {
        const shift = await database_1.prisma.shift.findUnique({
            where: { id: shiftId },
            include: {
                signups: {
                    where: { status: client_1.SignupStatus.CONFIRMED },
                },
            },
        });
        if (!shift) {
            throw new error_handler_1.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 email_service_1.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_1.logger.error(`Failed to send shift details to ${signup.userEmail}:`, err);
                failed++;
            }
        }
        return { sent, failed };
    },
    async getCalendarData(startDate, endDate) {
        const shifts = await database_1.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 = {};
        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 };
    },
};
//# sourceMappingURL=shifts.service.js.map