import {
    allPromises,
    BaseAdapter,
    ErrorCodes,
    Events,
    ListOptions,
    NotFoundError,
    OrderItem,
    RegionSiteUsersParams,
    RegionUsersParams,
    SiteUsersParams,
    TimeTrackingPolicyAssignedParams,
    TimeTrackingPolicyUnassignedParams,
    TimeTrackingPolicyUpdateParams,
    UserStatus,
} from '@humand-packages/common';
import { LimitOffsetPagination } from 'humand-packages/monolith/src/api/types/listOptions';
import { DateTime } from 'luxon';
import { Service } from 'typedi';
import { PortsDI } from '../../../../../config/portsDI';
import { EventEmitterService } from '../../../../../services/eventEmitter';
import { AttachmentsService } from '../../../../attachments/business/services/attachmentsService';
import { DepartmentsService } from '../../../../departments/business/services/departmentsService';
import { SiteService } from '../../../../regions/business/services/sitesService';
import { SiteUserService } from '../../../../regions/business/services/siteUserService';
import { UsersService } from '../../../../users/business/services/usersService';
import { UserListAllFilters } from '../../../../users/business/types/listAllFilters';
import TimeTrackingKioskUserRepositoryPort from '../../../infrastructure/ports/timeTracking/timeTrackingKioskUserPort';
import TimeTrackingKioskRepositoryPort from '../../../infrastructure/ports/TimeTrackingKioskRepositoryPort';
import { KioskUserDTO } from '../../../presentation/dtos/kioskUserDTO';
import { UserProfilePictureDTO } from '../../../presentation/dtos/UserProfilePictureDTO';
import { mapKioskUser } from '../../mappers/timeTracking/mapKioskUser';
import { TimeTrackingKioskUser, TimeTrackingKioskUserForCreation } from '../../models/timeTracking/timeTrackingKioskUser';
import { TimeTrackingPoliciesPort } from '../../ports/timeTrackingPoliciesPort';

const DEFAULT_PAGE_SIZE = 1000;
@Service()
export default class TimeTrackingKioskUserService extends BaseAdapter {
    constructor(
        @PortsDI(TimeTrackingKioskUserRepositoryPort)
        private readonly timeTrackingKioskUserRepository: TimeTrackingKioskUserRepositoryPort,
        private readonly siteUserService: SiteUserService,
        private readonly siteService: SiteService,
        private readonly eventEmitter: EventEmitterService,
        private readonly usersService: UsersService,
        @PortsDI(TimeTrackingKioskRepositoryPort) private readonly timeTrackingKioskRepository: TimeTrackingKioskRepositoryPort,
        @PortsDI(TimeTrackingPoliciesPort) private readonly timeTrackingPoliciesService: TimeTrackingPoliciesPort,
        private readonly departmentsService: DepartmentsService,
        private readonly attachmentsService: AttachmentsService,
    ) {
        super();
        this.setupEventHandlers();
    }

    private setupEventHandlers = () => {
        this.eventEmitter.listenEvent(Events.SITE_USERS_ASSIGNED, this.handleSiteUsersAssignedEvent);
        this.eventEmitter.listenEvent(Events.REGION_SITE_USERS_ASSIGNED, this.handleRegionSiteUsersAssignedEvent);
        this.eventEmitter.listenEvent(Events.SITE_USERS_UNASSIGNED, this.handleSiteUsersUnassignedEvent);
        this.eventEmitter.listenEvent(Events.SITE_DELETED, this.handleSiteDeletedEvent);
        this.eventEmitter.listenEvent(Events.REGION_DELETED, this.handleRegionDeletedEvent);
        this.eventEmitter.listenEvent(Events.REGION_USERS_UNASSIGNED, this.handleRegionUsersUnassignedEvent);
        this.eventEmitter.listenEvent(Events.TIME_TRACKING_POLICY_UNASSIGNED, this.handleTimeTrackingPolicyUnassignedEvent);
        this.eventEmitter.listenEvent(Events.TIME_TRACKING_POLICY_ASSIGNED, this.handleTimeTrackingPolicyAssignedEvent);
        this.eventEmitter.listenEvent(Events.TIME_TRACKING_POLICY_UPDATED, this.handleTimeTrackingPolicyUpdatedEvent);
    };

    public listUsers = async (
        botAppId: number,
        instanceId: number,
        since: Date | undefined,
        pagination: LimitOffsetPagination,
    ): Promise<{ count: number; lastUpdatedAt: Date | null; rows: KioskUserDTO[] }> => {
        const kiosk = await this.timeTrackingKioskRepository.getByBotAppId(botAppId, instanceId);
        if (!kiosk) throw new NotFoundError('Kiosk not found', ErrorCodes.NOT_FOUND);
        const { count: kioskUsersCount, rows: kioskUsers } = since
            ? await this.timeTrackingKioskUserRepository.getChangedByDate(instanceId, kiosk.siteId, since, pagination)
            : await this.timeTrackingKioskUserRepository.getUserIdsBySiteOrGlobal(instanceId, kiosk.siteId, pagination);
        if (kioskUsers.length === 0) return { count: 0, lastUpdatedAt: null, rows: [] };
        const userIds = [];
        const kioskUserByIdMap = new Map<number, TimeTrackingKioskUser>();
        let lastUpdatedAt: Date | null = null;
        for (const user of kioskUsers) {
            userIds.push(user.userId);
            kioskUserByIdMap.set(user.userId, user);
            const effectiveChangeTime = user.deletedAt && user.deletedAt > user.updatedAt ? user.deletedAt : user.updatedAt;
            if (!lastUpdatedAt || effectiveChangeTime > lastUpdatedAt) {
                lastUpdatedAt = effectiveChangeTime;
            }
        }
        const sortingOptions = [['firstName', 'asc'] as OrderItem, ['lastName', 'asc'] as OrderItem];
        const userPagination = { pagination: { limit: kioskUsers.length, offset: 0 }, sortingOptions } as ListOptions;
        const userFilters = {
            ids: userIds,
        } as UserListAllFilters;
        const { rows: userRows } = await this.usersService.listAllByInstance(instanceId, userFilters, userPagination);
        const departmentMembers = await this.departmentsService.getDepartmentMembersByUserIds(userIds, instanceId);
        userRows.forEach((user) => {
            user.department = departmentMembers.get(user.id);
        });
        const adjustedLastUpdatedAt = lastUpdatedAt ? DateTime.fromJSDate(lastUpdatedAt).plus({ milliseconds: 1 }).toJSDate() : null;
        return {
            count: kioskUsersCount,
            lastUpdatedAt: adjustedLastUpdatedAt,
            rows: userRows.map((user) => mapKioskUser(user, kioskUserByIdMap.get(user.id)!)),
        };
    };

    private handleTimeTrackingPolicyUnassignedEvent = async (params: TimeTrackingPolicyUnassignedParams) => {
        const { instanceId, userIds } = params;
        if (userIds.length === 0) return;
        const toDelete = await this.filterUsersWithoutActiveKioskPolicy(instanceId, userIds);
        if (toDelete.length === 0) return;
        await this.timeTrackingKioskUserRepository.bulkDeleteByUserIds(instanceId, toDelete);
    };

    private handleTimeTrackingPolicyAssignedEvent = async (params: TimeTrackingPolicyAssignedParams) => {
        const { instanceId, userIds } = params;
        if (userIds.length === 0) return;
        if (params.policy.clockingInKioskEnabled) {
            await this.createBulkKioskUsers(instanceId, userIds);
            return;
        }
        const toDelete = await this.filterUsersWithoutActiveKioskPolicy(instanceId, userIds);
        if (toDelete.length === 0) return;
        await this.timeTrackingKioskUserRepository.bulkDeleteByUserIds(instanceId, toDelete);
    };

    private filterUsersWithoutActiveKioskPolicy = async (instanceId: number, userIds: number[]): Promise<number[]> => {
        const stillKioskEnabled = new Set(await this.timeTrackingPoliciesService.getClockingInKioskEnabledUserIds(instanceId, userIds));
        return userIds.filter((id) => !stillKioskEnabled.has(id));
    };

    private handleTimeTrackingPolicyUpdatedEvent = async (params: TimeTrackingPolicyUpdateParams) => {
        const { instanceId, userIds, oldPolicy, newPolicy } = params;
        if (oldPolicy.clockingInKioskEnabled === newPolicy.clockingInKioskEnabled) return;
        if (userIds.length === 0) return;
        if (newPolicy.clockingInKioskEnabled) {
            await this.createBulkKioskUsers(instanceId, userIds);
            return;
        }
        const toDelete = await this.filterUsersWithoutActiveKioskPolicy(instanceId, userIds);
        if (toDelete.length === 0) return;
        await this.timeTrackingKioskUserRepository.bulkDeleteByUserIds(instanceId, toDelete);
    };

    private filterUsers = async (instanceId: number, userIds: number[]): Promise<number[]> => {
        const userIdsActives = await this.filterUserActives(instanceId, userIds);
        if (userIdsActives.length === 0) return [];
        const userIdsWithKioskEnabled = await this.timeTrackingPoliciesService.getClockingInKioskEnabledUserIds(instanceId, userIdsActives);
        return userIdsWithKioskEnabled;
    };

    private createBulkKioskUsers = async (instanceId: number, userIds: number[]): Promise<void> => {
        const filteredUserIds = await this.filterUsers(instanceId, userIds);
        if (filteredUserIds.length === 0) return;
        const uniqueUserIds = [...new Set(filteredUserIds)];
        const { siteIds, userSitesMap } = await this.getUserSitesMap(uniqueUserIds);
        const siteForRegionMap = await this.getSiteForRegionMap(instanceId, siteIds);
        const kioskUserForCreation = this.getKioskUserForCreation(instanceId, uniqueUserIds, userSitesMap, siteForRegionMap);
        await this.createBulkKioskUsersByBatch(kioskUserForCreation);
    };

    private handleRegionUsersUnassignedEvent = async (params: RegionUsersParams) => {
        const { instanceId, regionId, userIds } = params;
        const uniqueUserIds = [...new Set(userIds)];
        await this.timeTrackingKioskUserRepository.bulkDeleteByRegionId(instanceId, regionId, uniqueUserIds);
    };

    private handleRegionDeletedEvent = async (params: { regionId: number; instanceId: number; siteIds: number[] }) => {
        const { instanceId, regionId } = params;
        await this.timeTrackingKioskUserRepository.bulkDeleteByRegionId(instanceId, regionId);
    };

    private handleSiteDeletedEvent = async (params: { siteId: number; instanceId: number }) => {
        const { instanceId, siteId } = params;
        await this.timeTrackingKioskUserRepository.bulkDeleteBySiteId(instanceId, siteId);
    };

    private handleRegionSiteUsersAssignedEvent = async (params: RegionSiteUsersParams) => {
        const { instanceId, siteIds, userIds } = params;
        const filteredUserIds = await this.filterUsers(instanceId, userIds);
        if (filteredUserIds.length === 0) return;
        const uniqueUserIds = [...new Set(filteredUserIds)];
        const sites = await this.siteService.getByIds(siteIds, instanceId);
        const kioskUsersForCreation: TimeTrackingKioskUserForCreation[] = [];
        for (const site of sites) {
            for (const userId of uniqueUserIds) {
                kioskUsersForCreation.push(
                    new TimeTrackingKioskUserForCreation({ instanceId, regionId: site.getRegionId(), siteId: site.getId(), userId }),
                );
            }
        }
        await this.createBulkKioskUsersByBatch(kioskUsersForCreation);
    };

    private handleSiteUsersAssignedEvent = async (params: SiteUsersParams) => {
        const { instanceId, siteId, userIds } = params;
        const filteredUserIds = await this.filterUsers(instanceId, userIds);
        if (filteredUserIds.length === 0) return;
        const uniqueUserIds = [...new Set(filteredUserIds)];
        const site = await this.siteService.getById(siteId, instanceId);
        if (!site) throw new NotFoundError(`Site not found for site ID ${siteId}`, ErrorCodes.NOT_FOUND);
        const regionId = site.getRegionId();
        const kioskUsersForCreation = uniqueUserIds.map((userId) => new TimeTrackingKioskUserForCreation({ instanceId, regionId, siteId, userId }));
        await this.createBulkKioskUsersByBatch(kioskUsersForCreation);
    };

    private handleSiteUsersUnassignedEvent = async (params: SiteUsersParams) => {
        const { instanceId, siteId, userIds } = params;
        await this.timeTrackingKioskUserRepository.bulkDeleteBySiteId(instanceId, siteId, userIds);
    };

    public createKioskUserSnapshot = async (instanceId: number): Promise<void> => {
        const userIds = await this.usersService.getUserIdsInInstance(instanceId, [UserStatus.ACTIVE, UserStatus.UNCLAIMED]);
        await this.createBulkKioskUsers(instanceId, userIds);
    };

    private createBulkKioskUsersByBatch = async (kioskUserForCreation: TimeTrackingKioskUserForCreation[]): Promise<void> => {
        const kioskUserForCreationChunks = this.chunkArray(kioskUserForCreation, DEFAULT_PAGE_SIZE);
        await allPromises(
            kioskUserForCreationChunks.map((kioskUserForCreationChunk) => async () => {
                await this.timeTrackingKioskUserRepository.bulkCreate(kioskUserForCreationChunk);
            }),
        );
    };

    private getSiteForRegionMap = async (instanceId: number, siteIds: number[]): Promise<Map<number, number>> => {
        const siteIdChunks = this.chunkArray(siteIds, DEFAULT_PAGE_SIZE);
        const results = await allPromises(
            siteIdChunks.map((siteIdChunk) => async () => {
                return await this.siteService.getByIds(siteIdChunk, instanceId);
            }),
        );
        const allSites = results.flat();

        const siteForRegionMap = new Map<number, number>();
        for (const site of allSites) {
            siteForRegionMap.set(site.getId(), site.getRegionId());
        }
        return siteForRegionMap;
    };

    private getKioskUserForCreation = (
        instanceId: number,
        userIds: number[],
        userSitesMap: Map<number, number[]>,
        siteForRegionMap: Map<number, number>,
    ): TimeTrackingKioskUserForCreation[] => {
        const kioskUserForCreation: TimeTrackingKioskUserForCreation[] = [];
        for (const userId of userIds) {
            kioskUserForCreation.push(new TimeTrackingKioskUserForCreation({ instanceId, userId }));
            const siteIds = userSitesMap.get(userId) || [];
            for (const siteId of siteIds) {
                const regionId = siteForRegionMap.get(siteId);
                if (!regionId) throw new NotFoundError(`Region ID is required for site ID ${siteId}`, ErrorCodes.NOT_FOUND);
                kioskUserForCreation.push(new TimeTrackingKioskUserForCreation({ instanceId, regionId, siteId, userId }));
            }
        }
        return kioskUserForCreation;
    };

    private getUserSitesMap = async (userIds: number[]): Promise<{ siteIds: number[]; userSitesMap: Map<number, number[]> }> => {
        const userIdChunks = this.chunkArray(userIds, DEFAULT_PAGE_SIZE);
        const results = await allPromises(
            userIdChunks.map((userIdChunk) => async () => {
                return await this.siteUserService.getSitesByUserIds(userIdChunk);
            }),
        );
        const allSiteUsers = results.flat();

        const userSitesMap = new Map<number, number[]>();
        const siteIdsSet = new Set<number>();
        for (const siteUser of allSiteUsers) {
            const userId = siteUser.getUserId();
            const siteId = siteUser.getSiteId();
            const existingSites = userSitesMap.get(userId) || [];
            userSitesMap.set(userId, [...existingSites, siteId]);
            siteIdsSet.add(siteId);
        }
        return { siteIds: Array.from(siteIdsSet), userSitesMap };
    };

    private filterUserActives = async (instanceId: number, userIds: number[]): Promise<number[]> => {
        const userIdChunks = this.chunkArray(userIds, DEFAULT_PAGE_SIZE);
        const results = await allPromises(
            userIdChunks.map((userIdChunk) => async () => {
                const userFilters = {
                    ids: userIdChunk,
                    status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED],
                } as UserListAllFilters;
                const { rows } = await this.usersService.listAllByInstance(instanceId, userFilters, {
                    pagination: { limit: DEFAULT_PAGE_SIZE, offset: 0 },
                });
                return rows;
            }),
        );
        const allRows = results.flat();
        const filteredIds = allRows.map((row) => row.id);
        return filteredIds;
    };

    public createSnapshot = async (instanceIds: number[]): Promise<void> => {
        for (const instanceId of instanceIds) {
            const userIds = await this.usersService.getUserIdsInInstance(instanceId, [UserStatus.ACTIVE, UserStatus.UNCLAIMED]);
            await this.createBulkKioskUsers(instanceId, userIds);
        }
    };

    private chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
        const chunks: T[][] = [];
        for (let i = 0; i < array.length; i += chunkSize) {
            chunks.push(array.slice(i, i + chunkSize));
        }
        return chunks;
    };

    public listUsersProfilePicture = async (
        instanceId: number,
        userIds: number[],
        pagination: LimitOffsetPagination,
    ): Promise<{ count: number; rows: UserProfilePictureDTO[] }> => {
        const userPagination = { pagination: { limit: pagination.limit, offset: pagination.offset }, sortingOptions: [] } as ListOptions;
        const userFilters = {
            ids: userIds,
        } as UserListAllFilters;
        const { rows } = await this.usersService.listAllByInstance(instanceId, userFilters, userPagination);
        await this.attachmentsService.addSignedUrlToModelWithImages(instanceId, rows, ['profilePicture']);
        return { count: rows.length, rows: rows.map((row) => new UserProfilePictureDTO(row.id, row.profilePicture)) };
    };
}
