import { getMapByGetter, OrderItem, BaseAdapter, ListOptions, UserStatus } from '@humand-packages/common';
import moment from 'moment';
import { Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { SQLSetOperation } from '../../../../types/sequelize';
import { AttachmentsService } from '../../../attachments/business/services/attachmentsService';
import { Instance } from '../../../instances/business/models/instance';
import { Site } from '../../../regions/business/models/site';
import { SiteService } from '../../../regions/business/services/sitesService';
import { User } from '../../../users/business/models/user';
import { UserListAllFilters } from '../../../users/business/types/listAllFilters';
import { Incidence } from '../../infrastructure/constants/incidences';
import { hasLocationIncidence } from '../helpers/siteHelper';
import { TimeTrackingDaySummaryFiltersWithPagination } from '../interfaces/timeTrackingDaySummaryFilters';
import { EmployeeSummary } from '../models/employeeSummary';
import { TimeTrackingEmployeePort } from '../ports/employeeTablePorts';
import { TimeTrackingPoliciesPort } from '../ports/timeTrackingPoliciesPort';
import { UsersServicePort } from '../ports/userServicePort';
import { TimeTrackingEntrySource } from '../types/source';
import TimeTrackingService from './timeTracking';

@Service()
export default class TimeTrackingEmployeeAdapter extends BaseAdapter implements TimeTrackingEmployeePort {
    constructor(
        @PortsDI(TimeTrackingService) private readonly timeTrackingService: TimeTrackingService,
        @PortsDI(UsersServicePort) private readonly usersService: UsersServicePort,
        private readonly attachmentsService: AttachmentsService,
        private readonly sitesService: SiteService,
        @PortsDI(TimeTrackingPoliciesPort) private readonly timeTrackingPoliciesService: TimeTrackingPoliciesPort,
    ) {
        super();
    }

    //Will deprecate when front add exclude logic in schedule code and department
    async listEmployeeSummaries(
        user: User,
        instance: Instance,
        reportFilters: TimeTrackingDaySummaryFiltersWithPagination,
        filterWithPolicies = false,
        hasManageCapability = false,
    ): Promise<{ count: number; rows: EmployeeSummary[]; sites: Map<number, Site> }> {
        const emptyResult = { count: 0, rows: [], sites: new Map<number, Site>() };
        const userIds = await this.getFilteredUserIds(user, instance, reportFilters, filterWithPolicies, hasManageCapability);

        if (!userIds.length) return emptyResult;

        const sortingOptions = [['status', 'asc'] as OrderItem, ['firstName', 'asc'] as OrderItem, ['lastName', 'asc'] as OrderItem];
        const status = this.getUserStatusFilter(reportFilters.includeDeactivated);
        const { count, rows: employees } = await this.usersService.getUsers(
            user,
            { ids: userIds, status },
            { pagination: { limit: reportFilters.limit, offset: reportFilters.offset }, sortingOptions },
            { excludeLoggedUser: false },
        );
        const summaries: EmployeeSummary[] = [];
        const allDaySummaries = [];
        for (const employee of employees) {
            const daySummaries = await this.timeTrackingService.getOrCreateDaySummariesByUser(
                employee.id,
                instance,
                reportFilters.fromDate,
                reportFilters.toDate,
            );
            const incidencesTagsPresent = new Set<Incidence>();
            let estimatedHours = 0;
            let workedHours = 0;
            let differenceHours = 0;
            let extraHours = 0;
            daySummaries.forEach((daySummary) => {
                delete daySummary.createPolicyHistory;
                estimatedHours += daySummary.estimatedHours;
                workedHours += daySummary.workedHours;
                differenceHours += daySummary.workedHours - daySummary.estimatedHours;
                extraHours += daySummary.extraHours;
                const isPastDay = moment.tz(daySummary.dateString, instance.getTimezone()).isBefore(moment.tz(instance.getTimezone()), 'day');
                daySummary.timeTrackingEntries?.forEach((entry) => {
                    if (entry.source === TimeTrackingEntrySource.AUTO_CLOSE) daySummary.incidences.push(Incidence.AUTO_CLOSE);
                });
                if (!isPastDay) {
                    daySummary.incidences = daySummary.incidences.filter((tag) => tag !== Incidence.ABSENT && tag !== Incidence.UNDERWORKED);
                }
                daySummary.incidences.forEach((tag) => incidencesTagsPresent.add(tag));
                // Sort time off requests by id in descending order
                daySummary.timeOffRequests?.sort((a, b) => b.id - a.id);
            });
            if (extraHours) incidencesTagsPresent.add(Incidence.EXTRA_HOURS);
            allDaySummaries.push(...daySummaries);
            const { day, status } = await this.timeTrackingService.getUserActiveStatus(employee.id, instance);
            const employeeSummary = new EmployeeSummary(
                employee.id,
                employee.employeeInternalId,
                employee.getFullName(),
                employee.profilePicture,
                workedHours,
                estimatedHours,
                differenceHours,
                Array.from(incidencesTagsPresent),
                status,
                daySummaries,
                extraHours,
                day,
                employee.getStatus(),
            );
            summaries.push(employeeSummary);
        }
        const siteIds = new Set<number>();
        allDaySummaries
            .flatMap((summary) => summary.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());
        allDaySummaries.forEach((summary) => {
            summary.timeTrackingEntries?.forEach((entry) => {
                entry.locationIncidence = hasLocationIncidence(entry, siteByIds);
            });
        });
        if (summaries.length) await this.attachmentsService.addSignedUrlToModelWithImages(instance.getId(), summaries, ['profilePicture']);
        return { count, rows: summaries, sites: siteByIds };
    }

    async listEmployeeCalendars(
        user: User,
        instance: Instance,
        filters: TimeTrackingDaySummaryFiltersWithPagination,
    ): Promise<{ count: number; rows: EmployeeSummary[]; sites: Map<number, Site> }> {
        const emptyResult = { count: 0, rows: [], sites: new Map<number, Site>() };
        let userIds = await this.usersService.getUserIds(
            user,
            {
                departmentIds: filters.departmentIds,
                excludeDepartmentIds: filters.excludeDepartmentIds,
                ids: filters.userIds,
                search: filters.search,
                segmentationItemIds: filters.itemIds?.join(','),
            },
            true,
        );

        userIds = await this.timeTrackingService.searchUserIdsByDaySummaries({
            excludeScheduleCodes: filters.excludeScheduleCodes,
            fromDate: filters.fromDate,
            hasHolidays: filters.hasHolidays,
            hasSchedule: filters.hasSchedule,
            hasTimeOff: filters.hasTimeOff,
            incidences: filters.incidences,
            instanceId: instance.getId(),
            itemIds: filters.itemIds,
            onlyNoSchedule: filters.onlyNoSchedule,
            scheduleCodes: filters.scheduleCodes,
            status: filters.status,
            timezone: instance.getTimezone(),
            toDate: filters.toDate,
            userIds,
        });
        if (!userIds.length) return emptyResult;
        const sortingOptions = [['lastName', 'asc'] as OrderItem, ['firstName', 'asc'] as OrderItem];
        const pagination = { pagination: { limit: filters.limit, offset: filters.offset }, sortingOptions };
        const { count, rows: employees } = await this.usersService.getUsers(user, { ids: userIds }, pagination, { excludeLoggedUser: false });
        const summaries: EmployeeSummary[] = [];
        const allDaySummaries = [];
        for (const employee of employees) {
            const daySummaries = await this.timeTrackingService.getOrCreateDaySummariesByUser(
                employee.id,
                instance,
                filters.fromDate,
                filters.toDate,
            );
            const incidencesTagsPresent = new Set<Incidence>();
            let estimatedHours = 0;
            let workedHours = 0;
            let differenceHours = 0;
            let extraHours = 0;
            daySummaries.forEach((daySummary) => {
                estimatedHours += daySummary.estimatedHours;
                workedHours += daySummary.workedHours;
                differenceHours += daySummary.workedHours - daySummary.estimatedHours;
                extraHours += daySummary.extraHours;
                const isPastDay = moment.tz(daySummary.dateString, instance.getTimezone()).isBefore(moment.tz(instance.getTimezone()), 'day');
                if (!isPastDay) {
                    daySummary.incidences = daySummary.incidences.filter((tag) => tag !== Incidence.ABSENT && tag !== Incidence.UNDERWORKED);
                }
                daySummary.incidences.forEach((tag) => incidencesTagsPresent.add(tag));
                // Sort time off requests by id in descending order
                daySummary.timeOffRequests?.sort((a, b) => b.id - a.id);
            });
            allDaySummaries.push(...daySummaries);
            const { day, status } = await this.timeTrackingService.getUserActiveStatus(employee.id, instance);
            const employeeSummary = new EmployeeSummary(
                employee.id,
                employee.employeeInternalId,
                employee.getFullName(),
                employee.profilePicture,
                workedHours,
                estimatedHours,
                differenceHours,
                Array.from(incidencesTagsPresent),
                status,
                daySummaries,
                extraHours,
                day,
                employee.getStatus(),
            );
            summaries.push(employeeSummary);
        }
        const siteIds = new Set<number>();
        allDaySummaries
            .flatMap((summary) => summary.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());
        allDaySummaries.forEach((summary) => {
            summary.timeTrackingEntries?.forEach((entry) => {
                entry.locationIncidence = hasLocationIncidence(entry, siteByIds);
            });
        });
        if (summaries.length) await this.attachmentsService.addSignedUrlToModelWithImages(instance.getId(), summaries, ['profilePicture']);
        return { count, rows: summaries, sites: siteByIds };
    }

    public listUsers = async (
        loggedUser: User,
        filters: UserListAllFilters,
        { pagination, sortingOptions }: ListOptions,
        hasManageCapability = false,
    ) => {
        const emptyResult = { count: 0, rows: [] };
        let userIds = await this.usersService.getUserIds(loggedUser, filters, hasManageCapability);
        if (userIds.length) {
            userIds = await this.timeTrackingPoliciesService.getUserIdsWithPolicies(loggedUser.getInstanceId(), userIds);
        }
        if (!userIds.length) return emptyResult;
        const { rows, count } = await this.usersService.listUsers(
            loggedUser,
            { ids: userIds },
            { pagination, sortingOptions },
            { excludeLoggedUser: false },
        );
        if (rows.length) await this.attachmentsService.addSignedUrlToModelWithImages(loggedUser.instanceId, rows, ['profilePicture', 'coverPicture']);
        return { count, rows };
    };

    private async getFilteredUserIds(
        user: User,
        instance: Instance,
        reportFilters: TimeTrackingDaySummaryFiltersWithPagination,
        filterWithPolicies: boolean,
        hasManageCapability: boolean,
    ): Promise<number[]> {
        const collaboratorUserIds = new Set<number>();
        if (reportFilters.userIds?.length) {
            const userIds = await this.getUserIdsWithCollaboratorFilters(user, reportFilters, hasManageCapability);
            userIds.forEach((userId: number) => collaboratorUserIds.add(userId));
        }

        const hasSiteFilters = !!reportFilters.siteIds?.length;
        const hasSegmentationFilters = !!reportFilters.itemIds?.length;

        let siteUserIds: number[] = [];
        let segmentationUserIds: number[] = [];

        if (hasSiteFilters) {
            siteUserIds = await this.getUserIdsWithSiteFilters(user, instance, reportFilters, hasManageCapability);
        }
        if (hasSegmentationFilters) {
            segmentationUserIds = await this.getUserIdsWithSegmentationFilters(user, reportFilters, hasManageCapability);
        }

        // AND logic between site and segmentation filters
        const filteredUserIds = this.filterUsersBySiteAndSegmentation(siteUserIds, segmentationUserIds, hasSiteFilters, hasSegmentationFilters);
        filteredUserIds.forEach((userId) => collaboratorUserIds.add(userId));

        let searchUserIds: number[] = [];
        if (!hasSiteFilters && !hasSegmentationFilters && !reportFilters.userIds?.length) {
            searchUserIds = await this.getUserIdsWithSearchFilters(user, reportFilters, hasManageCapability);
            searchUserIds.forEach((userId: number) => collaboratorUserIds.add(userId));
        }

        let userIds = Array.from(collaboratorUserIds);

        // Apply policy filtering to all final users (both collaborators and site/segment filtered)
        if (userIds.length && filterWithPolicies) {
            userIds = await this.timeTrackingPoliciesService.getUserIdsWithPolicies(instance.getId(), userIds);
        }

        userIds = await this.timeTrackingService.searchUserIdsByDaySummaries({
            fromDate: reportFilters.fromDate,
            hasHolidays: reportFilters.hasHolidays,
            hasTimeOff: reportFilters.hasTimeOff,
            incidences: reportFilters.incidences,
            instanceId: instance.getId(),
            itemIds: reportFilters.itemIds,
            scheduleCodes: reportFilters.scheduleCodes,
            status: reportFilters.status,
            timezone: instance.getTimezone(),
            toDate: reportFilters.toDate,
            userIds,
        });
        return userIds;
    }

    private getUserStatusFilter(includeDeactivated?: boolean): UserStatus[] {
        return includeDeactivated ? [UserStatus.ACTIVE, UserStatus.UNCLAIMED, UserStatus.DEACTIVATED] : [UserStatus.ACTIVE, UserStatus.UNCLAIMED];
    }

    private async getUserIdsWithCollaboratorFilters(
        user: User,
        reportFilters: TimeTrackingDaySummaryFiltersWithPagination,
        hasManageCapability: boolean,
    ): Promise<number[]> {
        return await this.usersService.getUserIds(
            user,
            {
                departmentIds: reportFilters.departmentIds,
                excludeDepartmentIds: reportFilters.excludeDepartmentIds,
                ids: reportFilters.userIds,
                search: reportFilters.search,
                status: this.getUserStatusFilter(reportFilters.includeDeactivated),
            },
            hasManageCapability,
        );
    }

    private async getUserIdsWithSearchFilters(
        user: User,
        reportFilters: TimeTrackingDaySummaryFiltersWithPagination,
        hasManageCapability: boolean,
    ): Promise<number[]> {
        return await this.usersService.getUserIds(
            user,
            {
                departmentIds: reportFilters.departmentIds,
                excludeDepartmentIds: reportFilters.excludeDepartmentIds,
                search: reportFilters.search,
                status: this.getUserStatusFilter(reportFilters.includeDeactivated),
            },
            hasManageCapability,
        );
    }

    private async getUserIdsWithSiteFilters(
        user: User,
        instance: Instance,
        reportFilters: TimeTrackingDaySummaryFiltersWithPagination,
        hasManageCapability: boolean,
    ): Promise<number[]> {
        const userIds = await this.usersService.getUserIds(
            user,
            {
                departmentIds: reportFilters.departmentIds,
                excludeDepartmentIds: reportFilters.excludeDepartmentIds,
                search: reportFilters.search,
                status: this.getUserStatusFilter(reportFilters.includeDeactivated),
            },
            hasManageCapability,
        );
        if (!userIds.length) return [];
        return await this.timeTrackingService.filterUsersBySites(
            instance.getId(),
            userIds,
            reportFilters.fromDate,
            reportFilters.toDate,
            reportFilters.siteIds,
        );
    }

    private async getUserIdsWithSegmentationFilters(
        user: User,
        reportFilters: TimeTrackingDaySummaryFiltersWithPagination,
        hasManageCapability: boolean,
    ): Promise<number[]> {
        return await this.usersService.getUserIds(
            user,
            {
                departmentIds: reportFilters.departmentIds,
                excludeDepartmentIds: reportFilters.excludeDepartmentIds,
                itemIdFilterOperation: SQLSetOperation.UNION,
                search: reportFilters.search,
                segmentationItemIds: reportFilters.itemIds?.join(','),
                status: this.getUserStatusFilter(reportFilters.includeDeactivated),
            },
            hasManageCapability,
        );
    }

    private filterUsersBySiteAndSegmentation(
        siteUserIds: number[],
        segmentationUserIds: number[],
        hasSiteFilters: boolean,
        hasSegmentationFilters: boolean,
    ): number[] {
        if (hasSiteFilters && hasSegmentationFilters) {
            const siteUserSet = new Set(siteUserIds);
            return segmentationUserIds.filter((userId) => siteUserSet.has(userId));
        } else if (hasSiteFilters) {
            return siteUserIds;
        } else if (hasSegmentationFilters) {
            return segmentationUserIds;
        }
        return [];
    }
}
