import { BaseAdapter, ErrorCodes, Events, ISOLATION_LEVELS, RequestConflictError } from '@humand-packages/common';
import { Service } from 'typedi';
import { PortsDI } from '../../../../../config/portsDI';
import { EventEmitterService } from '../../../../../services/eventEmitter';
import { FeatureFlagCode } from '../../../../featureFlags/business/enums/featureFlagCode';
import { FeatureFlagService } from '../../../../featureFlags/business/services/featureFlagService';
import { TimeTrackingConfigRepositoryPort } from '../../../infrastructure/ports/timeTracking/timeTrackingConfigRepositoryPort';
import TimeTrackingUserSettingRepositoryPort from '../../../infrastructure/ports/timeTracking/timeTrackingUserSettingRepositoryPort';
import { TimeTrackingKioskPinBulkUpsertResult } from '../../../presentation/dtos/timeTracking/timeTrackingKioskPinBulkUpsertResult';
import { TimeTrackingKioskPinBulkUpsertInterface } from '../../interfaces/timeTracking/timeTrackingKioskPinBulkUpsertInterface';
import {
    TimeTrackingUserSetting,
    TimeTrackingUserSettingForCreation,
    TimeTrackingUserSettingWithKioskPolicy,
} from '../../models/timeTracking/timeTrackingUserSetting';
import { TimeTrackingPoliciesPort } from '../../ports/timeTrackingPoliciesPort';
import { UsersServicePort } from '../../ports/userServicePort';
import { SkippedTimeTrackingUserSetting } from '../../types/timeTracking/skippedTimeTrackingUserSetting';

@Service()
export default class TimeTrackingUserSettingService extends BaseAdapter {
    constructor(
        @PortsDI(TimeTrackingUserSettingRepositoryPort) private readonly timeTrackingUserSettingRepository: TimeTrackingUserSettingRepositoryPort,
        @PortsDI(UsersServicePort) private readonly usersService: UsersServicePort,
        private readonly eventEmitterService: EventEmitterService,
        private readonly featureFlagService: FeatureFlagService,
        @PortsDI(TimeTrackingConfigRepositoryPort) private readonly timeTrackingConfigService: TimeTrackingConfigRepositoryPort,
        @PortsDI(TimeTrackingPoliciesPort) private readonly timeTrackingPoliciesService: TimeTrackingPoliciesPort,
    ) {
        super();
        this.setupEventHandlers();
    }

    private setupEventHandlers = () => {
        this.eventEmitterService.listenEvent(Events.USERS_DELETION, this.handleUserDeletionEvent);
    };

    private handleUserDeletionEvent = async (params: { userIds: number[] }) => {
        await this.timeTrackingUserSettingRepository.deleteUserSettings(params.userIds);
    };

    public async updateKioskPin(instanceId: number, userId: number, kioskPin: string): Promise<TimeTrackingUserSetting> {
        const normalizedKioskPin = kioskPin.trim() ? kioskPin.trim() : null;
        if (normalizedKioskPin) {
            const alreadyExists = await this.timeTrackingUserSettingRepository.existsByInstanceIdAndKioskPin(instanceId, normalizedKioskPin, userId);
            if (alreadyExists) throw new RequestConflictError('Kiosk PIN already exists', ErrorCodes.TIME_TRACKING_KIOSK_PIN_ALREADY_EXISTS);
        }
        const user = await this.usersService.getByIdOrFail(instanceId, userId);
        const hasTimeTrackingDesertEagleEnabledFF = await this.getTimeTrackingDesertEagleEnabledFeatureFlag(instanceId);
        const userSetting = await this.timeTrackingUserSettingRepository.getByInstanceIdAndUserId(instanceId, user.getId());
        if (!userSetting) {
            const newUserSetting = this.timeTrackingUserSettingRepository.create({ instanceId, kioskPin: normalizedKioskPin, userId });
            if (hasTimeTrackingDesertEagleEnabledFF) {
                await this.timeTrackingConfigService.updateUserSetting(instanceId, userId, normalizedKioskPin);
            }
            return newUserSetting;
        } else {
            userSetting.kioskPin = normalizedKioskPin ?? null;
            const updatedUserSetting = await this.timeTrackingUserSettingRepository.update(userSetting);
            if (hasTimeTrackingDesertEagleEnabledFF) {
                await this.timeTrackingConfigService.updateUserSetting(instanceId, userId, normalizedKioskPin);
            }
            return updatedUserSetting;
        }
    }

    public async getTimeTrackingUserSetting(instanceId: number, userId: number): Promise<TimeTrackingUserSetting> {
        const userSetting = await this.timeTrackingUserSettingRepository.getByInstanceIdAndUserId(instanceId, userId);
        if (!userSetting) {
            return {
                instanceId,
                kioskPin: null,
                userId,
            } as TimeTrackingUserSetting;
        } else {
            return userSetting;
        }
    }

    public async getTimeTrackingUserSettingWithKioskPolicy(instanceId: number, userId: number): Promise<TimeTrackingUserSettingWithKioskPolicy> {
        const [userSetting, kioskEnabledUserIds] = await Promise.all([
            this.getTimeTrackingUserSetting(instanceId, userId),
            this.timeTrackingPoliciesService.getClockingInKioskEnabledUserIds(instanceId, [userId]),
        ]);

        return new TimeTrackingUserSettingWithKioskPolicy({
            ...userSetting,
            hasKioskEnabledPolicy: kioskEnabledUserIds.includes(userId),
        });
    }

    public async getTimeTrackingUserSettingByKioskPin(instanceId: number, kioskPin: string): Promise<TimeTrackingUserSetting | null> {
        return this.timeTrackingUserSettingRepository.getByInstanceIdAndKioskPin(instanceId, kioskPin);
    }

    public async bulkUpsertKioskPin(
        instanceId: number,
        payload: TimeTrackingKioskPinBulkUpsertInterface,
    ): Promise<TimeTrackingKioskPinBulkUpsertResult> {
        const { userSettings, skippedList, kioskPinList, userIdMap } = this.processBulkUpsertPayload(instanceId, payload);
        const userIds = Array.from(userIdMap.keys());
        const validUserIds = await this.usersService.getValidUsersByIds(instanceId, userIds);
        const validUserIdsSet = new Set<number>(validUserIds);
        const invalidUserIds = userIds.filter((userId) => !validUserIdsSet.has(userId));
        if (invalidUserIds.length) {
            skippedList.push(
                ...invalidUserIds.map((userId) => ({
                    kioskPin: userIdMap.get(userId)!,
                    reason: this.getUserNotExistsReason(),
                    userId,
                })),
            );
        }

        if (!validUserIds.length) {
            return {
                processed: [],
                skipped: skippedList,
            };
        }
        return await this.timeTrackingUserSettingRepository.transactionWithRetries(
            async () => {
                const existingUserSettings = await this.timeTrackingUserSettingRepository.getByInstanceIdAndKioskPinList(instanceId, kioskPinList);

                const existingUserSettingsMap = new Map<string, number>();
                existingUserSettings.forEach((userSetting) => existingUserSettingsMap.set(userSetting.kioskPin!, userSetting.userId));

                const userIdsToNullify: number[] = [];
                const userSettingsToUpsert: TimeTrackingUserSettingForCreation[] = [];
                userSettings.forEach((userSetting) => {
                    if (validUserIdsSet.has(userSetting.userId)) {
                        const existingUserId = existingUserSettingsMap.get(userSetting.kioskPin!);
                        if (existingUserId && existingUserId !== userSetting.userId) {
                            if (validUserIdsSet.has(existingUserId)) {
                                userIdsToNullify.push(existingUserId);
                                userSettingsToUpsert.push(userSetting);
                            } else {
                                skippedList.push({
                                    kioskPin: userSetting.kioskPin!,
                                    reason: this.getKioskPinAlreadyExistsReason(),
                                    userId: userSetting.userId,
                                });
                            }
                        } else {
                            userSettingsToUpsert.push(userSetting);
                        }
                    }
                });

                if (userIdsToNullify.length) {
                    await this.timeTrackingUserSettingRepository.bulkNullifyKioskPin(instanceId, userIdsToNullify);
                }

                const processed = await this.timeTrackingUserSettingRepository.bulkUpsert(userSettingsToUpsert);

                return {
                    processed,
                    skipped: skippedList,
                };
            },
            {
                isolationLevel: ISOLATION_LEVELS.SERIALIZABLE,
                tries: 3,
            },
        );
    }

    private processBulkUpsertPayload(
        instanceId: number,
        payload: TimeTrackingKioskPinBulkUpsertInterface,
    ): {
        userSettings: TimeTrackingUserSettingForCreation[];
        skippedList: SkippedTimeTrackingUserSetting[];
        kioskPinList: string[];
        userIdMap: Map<number, string>;
    } {
        const kioskPinSet = new Set<string>();
        const userSettings: TimeTrackingUserSettingForCreation[] = [];
        const skippedList: SkippedTimeTrackingUserSetting[] = [];
        const userIdMap = new Map<number, string>();

        payload.users.forEach((user) => {
            userIdMap.set(user.userId, user.kioskPin);
            if (kioskPinSet.has(user.kioskPin)) {
                skippedList.push({
                    kioskPin: user.kioskPin,
                    reason: this.getKioskPinAlreadyExistsReason(),
                    userId: user.userId,
                });
            } else {
                kioskPinSet.add(user.kioskPin);
                userSettings.push({
                    instanceId,
                    kioskPin: user.kioskPin,
                    userId: user.userId,
                } as TimeTrackingUserSettingForCreation);
            }
        });

        return {
            kioskPinList: Array.from(kioskPinSet),
            skippedList,
            userIdMap,
            userSettings,
        };
    }

    private getKioskPinAlreadyExistsReason() {
        return {
            code: ErrorCodes.TIME_TRACKING_KIOSK_PIN_ALREADY_EXISTS.toString(),
            message: 'Kiosk PIN already exists',
        };
    }

    private getUserNotExistsReason() {
        return {
            code: ErrorCodes.USER_NOT_FOUND.toString(),
            message: 'User not exists',
        };
    }

    private getTimeTrackingDesertEagleEnabledFeatureFlag = async (instanceId: number): Promise<boolean> => {
        const featureFlag = await this.featureFlagService.getBooleanFeatureFlag(FeatureFlagCode.TIME_TRACKING_DESERT_EAGLE_ENABLED, instanceId);
        return featureFlag.value;
    };

    public async getTimeTrackingUserSettingsByInstanceId(instanceId: number): Promise<TimeTrackingUserSetting[]> {
        return this.timeTrackingUserSettingRepository.getByInstanceId(instanceId);
    }
}
