import { BadRequestError, BaseAdapter, ErrorCodes, ForbiddenError, ISO_DATE, NotFoundError, OrderItem, UserStatus } from '@humand-packages/common';
import moment from 'moment-timezone';
import { Service } from 'typedi';
import { PortsDI } from '../../../../../config/portsDI';
import { LimitOffsetPagination } from '../../../../../types/listOptions';
import { AttachmentsService } from '../../../../attachments/business/services/attachmentsService';
import { Instance } from '../../../../instances/business/models/instance';
import { OrganizationChartsService } from '../../../../organizationCharts/business/services/organizationChartsService';
import { SiteService } from '../../../../regions/business/services/sitesService';
import { User } from '../../../../users/business/models/user';
import { UsersService } from '../../../../users/business/services/usersService';
import { userDAOtoUser } from '../../../../users/infrastructure/mappers/usersMapper';
import ShiftAssignedDayRepositoryPort from '../../../infrastructure/ports/shiftManagement/shiftAssignedDayRepositoryPort';
import { ShiftManagementCalendarFiltersVC } from '../../../presentation/validationClasses/shiftManagement/shiftManagementCalendarFiltersVC';
import { ShiftMyCalendarVC } from '../../../presentation/validationClasses/shiftManagement/shiftMyCalendar';
import { mapDaySummariesToPublicApiShifts } from '../../mappers/shiftManagement/mapPublicApiShift';
import { mapShiftAssociations, mapShiftCalendar } from '../../mappers/shiftManagement/mapShiftCalendar';
import { TimeTrackingDaySummary } from '../../models/daySummary';
import { PublicApiAssignedShift } from '../../models/shiftManagement/publicApiShift';
import { ShiftAssignedDay } from '../../models/shiftManagement/shiftAssignedDay';
import { ShiftCalendar, ShiftMyCalendar, ShiftMyCalendarDay } from '../../models/shiftManagement/shiftCalendar';
import { searchByShift, searchByUser } from '../../types/shiftManagement/searchType';
import { TimeTrackingForShiftService } from '../timeTracking/timeTrackingForShift';
import { RotationOccurrenceService } from './rotationOccurrenceService';

@Service()
export default class ShiftCalendarService extends BaseAdapter {
    constructor(
        @PortsDI(ShiftAssignedDayRepositoryPort) private readonly shiftAssignedDayRepository: ShiftAssignedDayRepositoryPort,
        private readonly timeTrackingForShiftService: TimeTrackingForShiftService,
        private readonly organizationService: OrganizationChartsService,
        private readonly usersService: UsersService,
        private readonly siteService: SiteService,
        private readonly attachmentsService: AttachmentsService,
        private readonly rotationOccurrenceService: RotationOccurrenceService,
    ) {
        super();
    }

    public myCalendar = async (instance: Instance, userId: number, filters: ShiftMyCalendarVC): Promise<ShiftMyCalendar> => {
        const daySummaries = await this.timeTrackingForShiftService.getTimeTrackingDaySummaries(
            instance,
            [userId],
            filters.startDate,
            filters.endDate,
        );

        // Collect all shift assigned day IDs
        const shiftAssignedDayIds: number[] = [];
        daySummaries.forEach((daySummary) => {
            if (daySummary.shiftAssignedDayId) {
                shiftAssignedDayIds.push(daySummary.shiftAssignedDayId);
            }
        });

        // Fetch all shift assigned days at once
        const shiftAssignedDays = await this.getShiftAssignedDaysByIds(shiftAssignedDayIds);
        const shiftAssignedDayMap = new Map<number, ShiftAssignedDay>();
        shiftAssignedDays.forEach((shiftAssignedDay) => {
            shiftAssignedDayMap.set(shiftAssignedDay.id, shiftAssignedDay);
        });

        const shiftMyCalendar: ShiftMyCalendar = new ShiftMyCalendar({
            days: {},
        });

        // Process each day summary
        for (const daySummary of daySummaries) {
            const shiftAssignedDay = daySummary.shiftAssignedDayId ? shiftAssignedDayMap.get(daySummary.shiftAssignedDayId) : undefined;
            const shiftCalendarDay = this.mapShiftCalendarDay(daySummary, shiftAssignedDay);
            shiftMyCalendar.days[daySummary.dateString] = shiftCalendarDay;
        }

        return shiftMyCalendar;
    };

    public getShiftAssignedDaysByIds = async (shiftAssignedDayIds: number[]): Promise<ShiftAssignedDay[]> => {
        return this.shiftAssignedDayRepository.getShiftAssignedDaysByIds(shiftAssignedDayIds);
    };

    public publicApiCalendar = async (
        instance: Instance,
        employeeId: string,
        pagination: LimitOffsetPagination,
        dateFrom?: string,
        dateTo?: string,
    ): Promise<{ count: number; rows: PublicApiAssignedShift[] }> => {
        const user = await this.usersService.findByEmployeeInternalId(instance.getId(), employeeId, true);
        if (!user) {
            throw new NotFoundError('Employee not found', ErrorCodes.USER_NOT_FOUND);
        }

        const timezone = instance.getTimezone();
        const today = moment.tz(timezone).format(ISO_DATE);
        const resolvedFrom = dateFrom ?? dateTo ?? today;
        const resolvedTo = dateTo ?? dateFrom ?? today;

        if (moment(resolvedTo).isBefore(moment(resolvedFrom))) {
            throw new BadRequestError('dateTo must not be before dateFrom', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        const totalDays = moment(resolvedTo).diff(moment(resolvedFrom), 'days') + 1;

        if (pagination.offset >= totalDays) {
            return { count: totalDays, rows: [] };
        }

        const pageStart = moment(resolvedFrom).add(pagination.offset, 'days').format(ISO_DATE);
        const pageEnd = moment(resolvedFrom)
            .add(Math.min(pagination.offset + pagination.limit, totalDays) - 1, 'days')
            .format(ISO_DATE);

        const daySummaries = await this.timeTrackingForShiftService.getTimeTrackingDaySummaries(instance, [user.getId()], pageStart, pageEnd);

        const shiftAssignedDayIds = daySummaries.map((daySummary) => daySummary.shiftAssignedDayId).filter((id): id is number => !!id);

        const shiftAssignedDays = shiftAssignedDayIds.length
            ? await this.shiftAssignedDayRepository.getShiftAssignedDaysByIds(shiftAssignedDayIds)
            : [];
        const shiftAssignedDayMap = new Map<number, ShiftAssignedDay>();
        shiftAssignedDays.forEach((shiftAssignedDay) => {
            shiftAssignedDayMap.set(shiftAssignedDay.id, shiftAssignedDay);
        });

        return {
            count: totalDays,
            rows: mapDaySummariesToPublicApiShifts(daySummaries, shiftAssignedDayMap, employeeId),
        };
    };

    private mapShiftCalendarDay = (daySummary: TimeTrackingDaySummary, shiftAssignedDay?: ShiftAssignedDay): ShiftMyCalendarDay => {
        return {
            holidays: daySummary.holidays || [],
            isWorkday: daySummary.isWorkday,
            scheduleData: {
                shiftAssociations: shiftAssignedDay ? mapShiftAssociations(shiftAssignedDay) : [],
                source: daySummary.scheduleSource,
                timeSlots: shiftAssignedDay ? [] : daySummary.timeSlots || [],
            },
            timeOffs: daySummary.timeOffRequests || [],
        };
    };

    public calendar = async (
        instance: Instance,
        user: User,
        filters: ShiftManagementCalendarFiltersVC,
        hasManageShiftsCapability: boolean,
        pagination: LimitOffsetPagination,
    ): Promise<ShiftCalendar> => {
        // Subordinados AND Sites AND ( (Search de Users OR Search de Turnos) AND Segmentaciones OR UserIds )
        const { totalUsers, userIds } = await this.getUserIdsAndTotals(user, filters, hasManageShiftsCapability);

        if (!userIds.length || totalUsers === 0) return this.getCalendarEmptyResponse(pagination, totalUsers);

        filters.userIds = userIds;
        const { userMap, userIdsAfterPagination } = await this.getUsers(user, filters.userIds, pagination);

        const daySummaries = await this.timeTrackingForShiftService.getTimeTrackingDaySummaries(
            instance,
            userIdsAfterPagination,
            filters.startDate,
            filters.endDate,
        );

        const shiftAssignedDayIds: number[] = [];
        const daySummaryIdToShiftAssignedDayIdMap = new Map<number, number>();

        // Group daySummaries by userId and then by dateString
        const userDaySummariesMap = new Map<number, Map<string, TimeTrackingDaySummary>>();

        daySummaries.forEach((daySummary) => {
            // Collect shiftAssignedDayIds for later lookup
            if (daySummary.shiftAssignedDayId) {
                shiftAssignedDayIds.push(daySummary.shiftAssignedDayId);
                daySummaryIdToShiftAssignedDayIdMap.set(daySummary.id, daySummary.shiftAssignedDayId);
            }

            // Group by userId and dateString
            if (!userDaySummariesMap.has(daySummary.userId)) {
                userDaySummariesMap.set(daySummary.userId, new Map<string, TimeTrackingDaySummary>());
            }

            const userDaysMap = userDaySummariesMap.get(daySummary.userId)!;
            userDaysMap.set(daySummary.dateString, daySummary);
        });

        const shiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByIds(shiftAssignedDayIds);
        const shiftAssignedDayMap = new Map<number, ShiftAssignedDay>();
        shiftAssignedDays.forEach((shiftAssignedDay) => {
            shiftAssignedDayMap.set(shiftAssignedDay.id, shiftAssignedDay);
        });

        const shiftCalendar = mapShiftCalendar(
            totalUsers,
            userIds.length,
            userDaySummariesMap,
            userMap,
            shiftAssignedDayMap,
            daySummaryIdToShiftAssignedDayIdMap,
            pagination,
        );

        return shiftCalendar;
    };

    private async getUserIdsAndTotals(
        user: User,
        filters: ShiftManagementCalendarFiltersVC,
        hasManageShiftsCapability: boolean,
    ): Promise<{ totalUsers: number; userIds: number[] }> {
        let totalUsers = 0;
        let userIds: number[] = [];
        const hasSearchFilters = this.hasSearchFilters(filters);
        const hasUserIdsFilter = !!filters.userIds?.length;
        const hasSegmentationItemIdsFilter = !!filters.segmentationItemIds?.length;

        // subordinates or all if manage
        const activeStatuses = [UserStatus.ACTIVE, UserStatus.UNCLAIMED];
        if (hasManageShiftsCapability) {
            userIds = await this.usersService.getUserIdsInInstance(user.getInstanceId(), activeStatuses);
            totalUsers = userIds.length;
        } else {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            if (filters.userIds && filters.userIds.some((id) => !subordinateIds.includes(id) && user.id !== id)) {
                throw new ForbiddenError('The provided user has no access to this resource', ErrorCodes.ACCESS_DENIED);
            }
            if (!subordinateIds.length) return { totalUsers: 0, userIds: [] };

            userIds = await this.usersService.getFilteredIds(user, { ids: subordinateIds, status: activeStatuses }, { excludeLoggedUser: false });
            totalUsers = userIds.length;
        }

        const activeUserIds = [...userIds];

        // filter by site
        if (!!filters.siteId) {
            const site = await this.siteService.getById(filters.siteId, user.getInstanceId());
            const siteUserIds = site.getUsers().map((siteUser) => siteUser.getUserId());
            if (siteUserIds.length === 0) return { totalUsers: 0, userIds: [] };
            userIds = userIds.filter((userId) => siteUserIds.includes(userId));
            totalUsers = userIds.length;
        }

        // get users from search (shift and user fields)
        let searchResultUserIds: number[] = [];

        if (hasSearchFilters) {
            // add the specified users to the result (only if they are active)
            if (hasUserIdsFilter) {
                userIds = [...new Set([...userIds, ...filters.userIds!.filter((id) => activeUserIds.includes(id))])];
            }

            if (searchByUser(filters.searchType!)) {
                const userSearchIds = await this.usersService.getFilteredIds(
                    user,
                    {
                        ids: userIds,
                        search: filters.search,
                        status: activeStatuses,
                    },
                    { excludeLoggedUser: false },
                );

                searchResultUserIds = [...searchResultUserIds, ...userSearchIds];
            }

            if (searchByShift(filters.searchType!)) {
                const shiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByShiftName(
                    user.getInstanceId(),
                    filters.search!,
                    filters.startDate,
                    filters.endDate,
                );

                if (shiftAssignedDays.length > 0) {
                    const shiftUserIds = [...new Set(shiftAssignedDays.map((assignedDay: ShiftAssignedDay) => assignedDay.userId))];
                    const filteredShiftUserIds = shiftUserIds.filter((userId) => userIds.includes(userId));
                    searchResultUserIds = [...searchResultUserIds, ...filteredShiftUserIds];
                }
            }

            userIds = [...new Set(searchResultUserIds)];
        }

        // filter by segmentation
        if (hasSegmentationItemIdsFilter) {
            userIds = await this.usersService.getFilteredIds(
                user,
                {
                    ids: userIds,
                    segmentationItemIds: filters.segmentationItemIds?.join(','),
                    status: activeStatuses,
                },
                { excludeLoggedUser: false },
            );

            if (hasUserIdsFilter && !hasSearchFilters) {
                userIds = [...new Set([...userIds, ...filters.userIds!.filter((id) => activeUserIds.includes(id))])];
            }
        }

        if (hasUserIdsFilter && !hasSearchFilters && !hasSegmentationItemIdsFilter) {
            userIds = userIds.filter((userId) => filters.userIds?.includes(userId) ?? false);
        }

        // filter by rotation: keep only users who have a shift from the given rotation(s) in range
        if (filters.shiftRotationIds?.length) {
            const rotationUserIds = await this.rotationOccurrenceService.userIdsWithRotationInRange(
                user.getInstanceId(),
                filters.shiftRotationIds,
                filters.startDate,
                filters.endDate,
            );
            const rotationUserIdsSet = new Set(rotationUserIds);
            userIds = userIds.filter((userId) => rotationUserIdsSet.has(userId));
        }

        return { totalUsers, userIds };
    }

    private hasSearchFilters(filters: ShiftManagementCalendarFiltersVC): boolean {
        return !!filters.search;
    }

    private async getUsers(
        user: User,
        userIds: number[],
        pagination: LimitOffsetPagination,
    ): Promise<{ userMap: Map<number, User>; userIdsAfterPagination: number[] }> {
        const sortingOptions = [['firstName', 'asc'] as OrderItem, ['lastName', 'asc'] as OrderItem];
        const listOptions = { pagination: { limit: pagination.limit, offset: pagination.offset }, sortingOptions };
        const { rows } = await this.usersService.listAll(user, { ids: userIds, status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED] }, listOptions, {
            excludeLoggedUser: false,
        });
        if (rows.length) await this.attachmentsService.addSignedUrlToModelWithImages(rows[0].instanceId, rows, ['profilePicture', 'coverPicture']);
        const userMap = new Map<number, User>();
        const userIdsAfterPagination: number[] = [];
        rows.forEach((userDAO) => {
            const userData = userDAOtoUser(userDAO);
            userMap.set(userData.getId(), userData);
            userIdsAfterPagination.push(userData.getId());
        });
        return { userIdsAfterPagination, userMap };
    }

    private getCalendarEmptyResponse(pagination: LimitOffsetPagination, totalUsers: number): ShiftCalendar {
        return new ShiftCalendar({
            count: 0,
            items: [],
            limit: pagination.limit,
            page: pagination.offset,
            total: totalUsers,
            totalPages: 0,
        });
    }
}
