import { allPromises, BaseAdapter, getMapByGetter, ISO_DATE, LoggerPort } from '@humand-packages/common';
import { LimitOffsetPagination } from 'humand-packages/monolith/src/api/types/listOptions';
import moment from 'moment';
import { Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { Instance } from '../../../instances/business/models/instance';
import { SiteService } from '../../../regions/business/services/sitesService';
import { UsersService } from '../../../users/business/services/usersService';
import { mapDaySummaryToDaySummaryApi } from '../../presentation/mappers/mapDaySummaryToDaySummaryApi';
import TimeTrackingDaySummaryPublicApi from '../interfaces/timeTrackingDaySummaryPublicApi';
import { TimeTrackingHourCategorizationService } from './hourCategorizationService';
import TimeTrackingService from './timeTracking';

interface UserRangeInfo {
    startDate: string;
    endDate: string;
    count: number;
}

@Service()
export class PublicTimeTrackingServiceAdapter extends BaseAdapter {
    constructor(
        @PortsDI(TimeTrackingService) private readonly timeTrackingService: TimeTrackingService,
        private readonly usersService: UsersService,
        private readonly logger: LoggerPort,
        private readonly sitesService: SiteService,
        private readonly hourCategorizationService: TimeTrackingHourCategorizationService,
    ) {
        super();
    }

    public listDaySummaries = async (instance: Instance, params: TimeTrackingDaySummaryPublicApi, limitOffsetPagination: LimitOffsetPagination) => {
        const { employeeIds, startDate, endDate } = params;
        const users = await this.usersService.findAllByEmployeeInternalId(instance.getId(), employeeIds);
        if (users.length === 0) return { count: 0, rows: [] };
        const userIds = users.map((user) => user.id);
        const { userDateRangeMap } = this.getUserRangeMapPagination(userIds, instance.getTimezone(), startDate, endDate, limitOffsetPagination);
        if (userDateRangeMap.size === 0) return { count: 0, rows: [] };

        const userEmployeeMap = new Map<number, string>();
        users.forEach((user) => userEmployeeMap.set(user.id, user.employeeInternalId));

        const daySummaryPromises = [...userDateRangeMap.entries()].map(([userId, info]) => async () => {
            const list = await this.timeTrackingService.getOrCreateDaySummariesByUser(userId, instance, info.startDate, info.endDate, true);
            if (list.length !== info.count) {
                this.logger.warn(`User ${userId}: expected ${info.count} summaries, got ${list.length}`);
            }
            return list;
        });
        const chunkDaySummaries = await allPromises(daySummaryPromises, 5);
        const daySummaries = chunkDaySummaries.flat().sort((a, b) => a.dateString.localeCompare(b.dateString) || a.userId - b.userId);
        const siteIds = new Set<number>();
        daySummaries.forEach((daySummary) => {
            daySummary.timeTrackingEntries?.forEach((entry) => {
                if (entry.siteId) siteIds.add(entry.siteId);
            });
        });
        const sites = siteIds.size ? await this.sitesService.getByIds(Array.from(siteIds), instance.getId(), true) : [];
        const siteByIds = getMapByGetter(sites, (site) => site.getId());

        const categorizedHoursMap = await this.hourCategorizationService.getApprovedCategorizedHoursForDaySummaries(instance.getId(), daySummaries);
        return {
            count: daySummaries.length,
            rows: daySummaries.map((daySummary) =>
                mapDaySummaryToDaySummaryApi(daySummary, {
                    categorizedHours: categorizedHoursMap.get(daySummary.id) ?? [],
                    employeeId: userEmployeeMap.get(daySummary.userId) ?? '',
                    siteByIds: siteByIds,
                    timezone: instance.getTimezone(),
                }),
            ),
        };
    };

    private getUserRangeMapPagination = (
        userIds: number[],
        timezone: string,
        startDate: string,
        endDate: string,
        limitOffsetPagination: LimitOffsetPagination,
    ) => {
        const startDateMoment = moment.tz(startDate, ISO_DATE, timezone).startOf('day');
        const endDateMoment = moment.tz(endDate, ISO_DATE, timezone).startOf('day');
        const totalDays = endDateMoment.diff(startDateMoment, 'days') + 1;
        const totalItems = totalDays * userIds.length;
        const { offset, limit } = limitOffsetPagination;
        if (totalItems <= offset) return { count: totalItems, userDateRangeMap: new Map<number, UserRangeInfo>() };

        const userWindowDates = [];
        let cursor = 0;
        outer: for (let day = 0; day < totalDays; day++) {
            const date = startDateMoment.clone().add(day, 'days').format(ISO_DATE);
            for (const userId of userIds) {
                if (cursor >= offset && userWindowDates.length < limit) userWindowDates.push({ dateString: date, userId });
                cursor++;
                if (userWindowDates.length === limit) break outer;
            }
        }

        const userDateRangeMap = new Map<number, UserRangeInfo>();

        for (const { userId, dateString } of userWindowDates) {
            const userRange = userDateRangeMap.get(userId) ?? { count: 0, endDate: dateString, startDate: dateString };
            userRange.startDate = userRange.startDate > dateString ? dateString : userRange.startDate;
            userRange.endDate = userRange.endDate < dateString ? dateString : userRange.endDate;
            userRange.count++;
            userDateRangeMap.set(userId, userRange);
        }

        return { count: totalItems, userDateRangeMap };
    };
}
