import { LimitOffsetPagination } from '@humand/bamboo-grpc-client/dist/generated/humand/bamboo/v1/pagination';
import moment from 'moment';
import { User } from '../../../../users/business/models/user';
import { TimeTrackingDaySummary } from '../../models/daySummary';
import { TimeTrackingDaySummaryScheduleDataForShiftCalendar } from '../../models/daySummarySchedule';
import { ShiftWithAssignedData } from '../../models/shiftManagement/shift';
import { ShiftAssignedDay } from '../../models/shiftManagement/shiftAssignedDay';
import { ShiftAssignedShift } from '../../models/shiftManagement/shiftAssignedShift';
import {
    ShiftCalendar,
    ShiftCalendarUser,
    ShiftCalendarDay,
    ShiftScheduledHours,
    ShiftAssociation,
} from '../../models/shiftManagement/shiftCalendar';
import { TIME_FORMAT, TimeSlot } from '../../models/timeTracking/scheduleData';
import { DaySummaryScheduleSource } from '../../types/scheduleSource';
import { ShiftAssignedShiftSource } from '../../types/shiftManagement/source';

const MINUTES_IN_HOUR = 60;
const TIME_SLOTS_TIME_FORMAT = 'HH:mm';

export const mapShiftCalendar = (
    totalUsers: number,
    userCount: number,
    userDaySummariesMap: Map<number, Map<string, TimeTrackingDaySummary>>,
    userMap: Map<number, User>,
    shiftAssignedDayMap: Map<number, ShiftAssignedDay>,
    daySummaryIdToShiftAssignedDayIdMap: Map<number, number>,
    pagination: LimitOffsetPagination,
): ShiftCalendar => {
    const shiftCalendarUsers: ShiftCalendarUser[] = [];

    for (const [userId, userData] of userMap) {
        const userDaysMap = userDaySummariesMap.get(userId);
        if (!userDaysMap) continue;

        const days: Record<string, ShiftCalendarDay> = {};

        let draftHours = 0;
        let publishedHours = 0;
        let totalHours = 0;
        userDaysMap.forEach((daySummary, dateString) => {
            const shiftAssignedDayId = daySummaryIdToShiftAssignedDayIdMap.get(daySummary.id);
            const shiftAssignedDay = shiftAssignedDayId ? shiftAssignedDayMap.get(shiftAssignedDayId) : undefined;
            const shiftCalendarDay = mapShiftCalendarDay(daySummary, shiftAssignedDay);
            days[dateString] = shiftCalendarDay;
            draftHours += shiftCalendarDay.shiftScheduledHours.draft;
            publishedHours += shiftCalendarDay.shiftScheduledHours.published;
            totalHours += shiftCalendarDay.shiftScheduledHours.total;
        });

        shiftCalendarUsers.push(
            new ShiftCalendarUser({
                days: days,
                employeeInternalId: userData?.getEmployeeInternalId() || '',
                fullName: userData?.getFullName() || '',
                profilePicture: userData?.getProfilePicture() || null,
                shiftScheduleHours: new ShiftScheduledHours({
                    draft: draftHours,
                    published: publishedHours,
                    total: totalHours,
                }),
                userId: userId,
            }),
        );
    }

    const { page, totalPages } = paginateData(userCount, pagination.limit, pagination.offset);

    return new ShiftCalendar({
        count: userCount,
        items: shiftCalendarUsers,
        limit: pagination.limit,
        page: page,
        total: totalUsers,
        totalPages: totalPages,
    });
};

const mapShiftCalendarDay = (daySummary: TimeTrackingDaySummary, shiftAssignedDay?: ShiftAssignedDay): ShiftCalendarDay => {
    const timeSlots = getTimeSlotFromShiftAssignedDay(shiftAssignedDay);
    const duration = getScheduledHoursByTimeSlot(timeSlots || []);

    daySummary.timeSlots?.sort(sortTimeSlots);

    return new ShiftCalendarDay({
        daySummaryHours: {
            estimated: daySummary.estimatedHours,
            scheduled: daySummary.scheduledHours || 0,
            timeOff: daySummary.timeOffHours || 0,
            worked: daySummary.workedHours,
        },
        holidays: daySummary.holidays || [],
        scheduledData: new TimeTrackingDaySummaryScheduleDataForShiftCalendar({
            isWorkday: daySummary.isWorkday,
            scheduleSource:
                daySummary.scheduleSource === DaySummaryScheduleSource.SHIFT && shiftAssignedDay?.shiftAssociations?.length === 0
                    ? DaySummaryScheduleSource.NO_SCHEDULE
                    : daySummary.scheduleSource,
            shiftAssignedDayId: shiftAssignedDay?.id,
            timeSlots: daySummary.timeSlots,
        }),
        shiftScheduledHours: new ShiftScheduledHours({
            draft: duration,
            published: duration,
            total: duration,
        }),
        shifts: mapShiftsWithAssignedData(shiftAssignedDay),
        timeOffs: daySummary.timeOffRequests || [],
    });
};

const mapShiftsWithAssignedData = (shiftAssignedDay?: ShiftAssignedDay): ShiftWithAssignedData[] => {
    const shifts =
        shiftAssignedDay?.shiftAssociations
            ?.map((shiftAssociation) => {
                if (!(shiftAssociation as ShiftAssignedShift).id) {
                    return undefined;
                }
                return new ShiftWithAssignedData({
                    ...shiftAssociation.shift,
                    isFromShiftRotation: shiftAssociation.shiftRotationId != null,
                    shiftAssignedShiftId: (shiftAssociation as ShiftAssignedShift).id,
                    timeSlots:
                        shiftAssociation.source === ShiftAssignedShiftSource.APP
                            ? shiftAssociation.shift?.timeSlots || []
                            : shiftAssociation.timeSlots || [],
                    type: (shiftAssociation as ShiftAssignedShift).type,
                });
            })
            .filter((shift) => shift !== undefined) || [];

    return shifts.sort(compareShifts);
};

const compareShifts = (a: ShiftWithAssignedData, b: ShiftWithAssignedData) => {
    const earliestA = getEarliestSlot(a.timeSlots);
    const earliestB = getEarliestSlot(b.timeSlots);

    if (!earliestA && !earliestB) return 0;
    if (!earliestA) return 1;
    if (!earliestB) return -1;

    return sortTimeSlots(earliestA, earliestB);
};

const getEarliestSlot = (slots: TimeSlot[]): TimeSlot | null => {
    if (!slots || slots.length === 0) return null;
    return slots.slice().sort(sortTimeSlots)[0];
};

/**
 * Sorts time slots considering night shifts that cross midnight
 * Night shifts (where endTime < startTime) are sorted by their start time,
 * but positioned appropriately in the chronological sequence
 */
const sortTimeSlots = (a: TimeSlot, b: TimeSlot): number => {
    const startA = moment(a.startTime, TIME_SLOTS_TIME_FORMAT);
    const startB = moment(b.startTime, TIME_SLOTS_TIME_FORMAT);
    const endA = moment(a.endTime, TIME_SLOTS_TIME_FORMAT);
    const endB = moment(b.endTime, TIME_SLOTS_TIME_FORMAT);

    // Check if shifts are night shifts (cross midnight)
    const isNightShiftA = endA.isBefore(startA);
    const isNightShiftB = endB.isBefore(startB);

    // If both are night shifts, sort by start time
    if (isNightShiftA && isNightShiftB) {
        return startA.diff(startB);
    }

    // If only A is a night shift, it should come after day shifts that start before midnight
    // but before day shifts that would start after the night shift ends
    if (isNightShiftA && !isNightShiftB) {
        // If day shift B starts before the night shift A starts, B comes first
        if (startB.isBefore(startA)) {
            return 1;
        }
        // If day shift B starts after the night shift A ends (next day), A comes first
        const endANextDay = moment(a.endTime, TIME_SLOTS_TIME_FORMAT).add(1, 'day');
        if (startB.isAfter(endANextDay)) {
            return -1;
        }
        // Otherwise, sort by start time
        return startA.diff(startB);
    }

    // If only B is a night shift, reverse the logic
    if (!isNightShiftA && isNightShiftB) {
        // If day shift A starts before the night shift B starts, A comes first
        if (startA.isBefore(startB)) {
            return -1;
        }
        // If day shift A starts after the night shift B ends (next day), B comes first
        const endBNextDay = moment(b.endTime, TIME_SLOTS_TIME_FORMAT).add(1, 'day');
        if (startA.isAfter(endBNextDay)) {
            return 1;
        }
        // Otherwise, sort by start time
        return startA.diff(startB);
    }

    // Both are regular day shifts, sort by start time
    return startA.diff(startB);
};

const calculateDuration = (slot: TimeSlot) => {
    const start = moment(slot.startTime, TIME_FORMAT);
    const end = moment(slot.endTime, TIME_FORMAT);
    //If the end time is before the start time, it means it goes to the next day (night shift)
    if (end.isBefore(start)) {
        end.add(1, 'day');
    }
    return end.diff(start, 'minutes') / MINUTES_IN_HOUR;
};

const getScheduledHoursByTimeSlot = (timeSlots: TimeSlot[]) => {
    return timeSlots.map((slot) => calculateDuration(slot)).reduce((a, b) => a + b, 0);
};

const paginateData = (count: number, limit: number, offset: number) => {
    const expectingPage = Math.ceil((offset + 1) / limit);
    const page = isFinite(expectingPage) && expectingPage > 0 ? expectingPage : 1;
    const expectingTotalPages = limit ? Math.ceil(count / limit) : 1;
    const totalPages = isFinite(expectingTotalPages) && expectingTotalPages > 0 ? expectingTotalPages : 1;
    return { page, totalPages };
};

const getTimeSlotFromShiftAssignedDay = (shiftAssignedDay?: ShiftAssignedDay): TimeSlot[] => {
    const timeSlots: TimeSlot[] = [];
    shiftAssignedDay?.shiftAssociations?.forEach((shift) => {
        switch (shift.source) {
            case ShiftAssignedShiftSource.INTEGRATION:
                timeSlots.push(...(shift.timeSlots || []));
                break;
            case ShiftAssignedShiftSource.APP:
                timeSlots.push(...(shift.shift?.timeSlots || []));
                break;
            default:
                break;
        }
    });
    return timeSlots;
};

export const mapShiftAssociations = (shiftAssignedDay: ShiftAssignedDay): ShiftAssociation[] => {
    const shiftAssociations: ShiftAssociation[] = [];
    shiftAssignedDay.shiftAssociations?.forEach((shiftAssociation) => {
        const associationTimeSlots: TimeSlot[] =
            shiftAssociation.source === ShiftAssignedShiftSource.APP
                ? (shiftAssociation.shift?.timeSlots ?? []).map((ts) => ({ startTime: ts.startTime, endTime: ts.endTime }))
                : (shiftAssociation.timeSlots ?? []).map((ts) => ({ startTime: ts.startTime, endTime: ts.endTime }));
        if (associationTimeSlots.length > 0) {
            shiftAssociations.push({
                description: shiftAssociation.shift?.description ?? null,
                id: shiftAssociation.shift?.id ?? (shiftAssociation as ShiftAssignedShift).id,
                isFromShiftRotation: shiftAssociation.shiftRotationId != null,
                name: shiftAssociation.shift?.name ?? null,
                timeSlots: associationTimeSlots,
            });
        }
    });
    return shiftAssociations;
};
