import { ForbiddenError, NotFoundError, ErrorCodes, BotAppEventParams, Events, BaseAdapter, UserStatus } from '@humand-packages/common';
import { OrderItem } from 'sequelize';
import { Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { EventEmitterService } from '../../../../services/eventEmitter';
import ListOptions from '../../../../types/listOptions';
import { AttachmentsService } from '../../../attachments/business/services/attachmentsService';
import UserBotAppService from '../../../botApps/business/services/botApp';
import { DepartmentsService } from '../../../departments/business/services/departmentsService';
import { SiteService } from '../../../regions/business/services/sitesService';
import { User } from '../../../users/business/models/user';
import { UserInterface } from '../../../users/business/models/userInterface';
import { UserListAllFilters } from '../../../users/business/types/listAllFilters';
import { userDAOtoUserDTO } from '../../../users/infrastructure/mappers/usersMapper';
import { UserDTO } from '../../../users/presentation/dataTransferObjects/userDTO';
import TimeTrackingKiosksRepositoryPort from '../../infrastructure/ports/TimeTrackingKioskRepositoryPort';
import { KioskCollabsFilters } from '../interfaces/kioskCollabsFilters';
import KioskCollabsOptions from '../interfaces/kioskCollabsOptions';
import TimeTrackingKiosksFilters from '../interfaces/timeTrackingKioskFilters';
import TimeTrackingKiosksOptions from '../interfaces/timeTrackingKioskOptions';
import { TimeTrackingKiosk, TimeTrackingKioskForCreation } from '../models/timeTrackingKiosk';
import { TimeTrackingKioskLinkPort } from '../ports/timeTrackingKioskLinkPort';
import { TimeTrackingKioskPort } from '../ports/timeTrackingKioskPort';
import { TimeTrackingPoliciesPort } from '../ports/timeTrackingPoliciesPort';
import { UsersServicePort } from '../ports/userServicePort';
import { TimeTrackingKioskStatus } from '../types/kioskStatus';
import TimeTrackingUserSettingService from './timeTracking/timeTrackingUserSetting';

@Service()
export default class TimeTrackingKiosksAdapter extends BaseAdapter implements TimeTrackingKioskPort {
    constructor(
        @PortsDI(TimeTrackingKiosksRepositoryPort) private readonly timeTrackingKiosksRepository: TimeTrackingKiosksRepositoryPort,
        @PortsDI(UsersServicePort) private readonly usersService: UsersServicePort,
        private readonly departmentsService: DepartmentsService,
        private readonly eventEmitterService: EventEmitterService,
        private readonly botAppService: UserBotAppService,
        @PortsDI(TimeTrackingKioskLinkPort) private readonly timeTrackingKioskLinkService: TimeTrackingKioskLinkPort,
        @PortsDI(TimeTrackingPoliciesPort) private readonly timeTrackingPoliciesService: TimeTrackingPoliciesPort,
        private readonly attachmentsService: AttachmentsService,
        private readonly timeTrackingUserSettingService: TimeTrackingUserSettingService,
        private readonly siteService: SiteService,
    ) {
        super();
        this.setupEventHandlers();
    }

    private setupEventHandlers = () => {
        this.eventEmitterService.listenEvent(Events.BOT_APP_DELETION, this.handleBotAppDeletionEvent);
        this.eventEmitterService.listenEvent(Events.SITE_DELETED, this.handleSiteDeletedEvent);
    };

    private handleBotAppDeletionEvent = async (params: BotAppEventParams) => {
        await this.removeAssociation(params.botAppId, params.instanceId);
    };

    private removeAssociation = async (botAppId: number, instanceId: number) => {
        const kiosk = await this.timeTrackingKiosksRepository.getByBotAppId(botAppId, instanceId);
        if (kiosk) {
            kiosk.botAppId = undefined;
            await this.timeTrackingKiosksRepository.update(kiosk);
        }
    };

    public getByIdOrFail = async (user: UserInterface, id: number): Promise<TimeTrackingKiosk> => {
        return await this.timeTrackingKiosksRepository.getByIdOrFail(user.instanceId, id);
    };

    public getByBotAppId = async (botAppId: number, instanceId: number): Promise<TimeTrackingKiosk | null> => {
        return await this.timeTrackingKiosksRepository.getByBotAppId(botAppId, instanceId);
    };

    public getByBotAppIdOrFail = async (botAppId: number, instanceId: number): Promise<TimeTrackingKiosk> => {
        const kiosk = await this.timeTrackingKiosksRepository.getByBotAppId(botAppId, instanceId);
        if (!kiosk) {
            throw new NotFoundError('Kiosk not found', ErrorCodes.NOT_FOUND);
        }
        return kiosk;
    };

    public list = async (
        user: UserInterface,
        filters: TimeTrackingKiosksFilters,
        options: TimeTrackingKiosksOptions,
    ): Promise<{ count: number; rows: TimeTrackingKiosk[] }> => {
        return await this.timeTrackingKiosksRepository.list(user as User, filters, options);
    };

    public create = async (user: UserInterface, kiosk: TimeTrackingKioskForCreation): Promise<TimeTrackingKiosk> => {
        if (kiosk.siteId) {
            const site = await this.siteService.getById(kiosk.siteId!, user.instanceId);
            if (!site) {
                throw new NotFoundError('Site not found', ErrorCodes.NOT_FOUND);
            }
        }
        return this.timeTrackingKiosksRepository.getTransaction(async () => {
            return await this.timeTrackingKiosksRepository.create(user as User, kiosk);
        });
    };

    public update = async (user: UserInterface, id: number, newAttributes: Partial<TimeTrackingKioskForCreation>): Promise<TimeTrackingKiosk> => {
        const oldAttributes = await this.timeTrackingKiosksRepository.getByIdOrFail(user.instanceId, id);
        if (newAttributes.siteId) {
            const site = await this.siteService.getById(newAttributes.siteId!, user.instanceId);
            if (!site) {
                throw new NotFoundError('Site not found', ErrorCodes.NOT_FOUND);
            }
        }
        return this.timeTrackingKiosksRepository.getTransaction(async () => {
            const updatedKiosk = await this.timeTrackingKiosksRepository.update({
                ...oldAttributes,
                ...(Object.fromEntries(
                    Object.entries(newAttributes).filter(([_, value]) => value !== undefined),
                ) as Partial<TimeTrackingKioskForCreation>),
            });
            return updatedKiosk;
        });
    };

    public eliminate = async (id: number, instanceId: number) => {
        const kiosk = await this.timeTrackingKiosksRepository.getById(instanceId, id);
        return this.timeTrackingKiosksRepository.getTransaction(async () => {
            await this.timeTrackingKiosksRepository.eliminate(id, instanceId);
            if (kiosk && kiosk.botAppId) {
                await this.botAppService.delete(kiosk.botAppId, instanceId);
            }
        });
    };

    public listCollabs = async (instanceId: number, filters: KioskCollabsFilters, options: KioskCollabsOptions, botAppId: number) => {
        const userIdsEnabledForKiosk = await this.timeTrackingPoliciesService.getClockingInKioskEnabledUserIds(instanceId);
        let userIdsEnabledForKioskAndSite = userIdsEnabledForKiosk;
        const kiosk = await this.timeTrackingKiosksRepository.getByBotAppId(botAppId, instanceId);
        if (kiosk?.siteId) {
            const kioskSiteUsers = await this.getUsersBySiteId(instanceId, kiosk.siteId);
            userIdsEnabledForKioskAndSite = userIdsEnabledForKiosk.filter((userId) => kioskSiteUsers.includes(userId));
        }
        const userIds = [...new Set([...(filters.userIds || []), ...userIdsEnabledForKioskAndSite])];
        if (!userIds.length) {
            return { count: 0, rows: [] };
        }
        const sortingOptions = [['firstName', 'asc'] as OrderItem, ['lastName', 'asc'] as OrderItem];
        const pagination = { pagination: { limit: options.limit, offset: options.offset }, sortingOptions } as ListOptions;
        const userFilters = {
            departmentIds: filters.departmentIds,
            ids: userIds,
            search: filters.q,
            segmentationItemIds: filters.itemIds?.join(','),
            status: [UserStatus.ACTIVE, UserStatus.UNCLAIMED],
        } as UserListAllFilters;

        const { count, rows } = await this.usersService.listAllByInstance(instanceId, userFilters, pagination);
        const departmentMembers = await this.departmentsService.getDepartmentMembersByUserIds(
            rows.map((user) => user.id),
            instanceId,
        );
        rows.forEach((user) => {
            user.department = departmentMembers.get(user.id);
        });
        if (rows.length) await this.attachmentsService.addSignedUrlToModelWithImages(instanceId, rows, ['profilePicture']);
        return { count, rows };
    };

    public unLink = async (id: number, instanceId: number): Promise<TimeTrackingKiosk> => {
        let botAppId: number | undefined;
        const kiosk = await this.timeTrackingKiosksRepository.getTransaction(async () => {
            const kiosk = await this.timeTrackingKiosksRepository.getByIdOrFail(instanceId, id);
            if (kiosk.status === TimeTrackingKioskStatus.UNLINKED) {
                throw new ForbiddenError('Kiosk is not linked', ErrorCodes.TIME_TRACKING_KIOSK_LINK_CODE_NOT_LINKED_ERROR);
            }
            if (kiosk.status === TimeTrackingKioskStatus.WAITING_FOR_LINK) {
                await this.timeTrackingKioskLinkService.expireWaitingLink(id, instanceId);
            }
            if (kiosk.status === TimeTrackingKioskStatus.ACTIVE) {
                botAppId = kiosk.botAppId;
            }
            kiosk.status = TimeTrackingKioskStatus.UNLINKED;
            return await this.timeTrackingKiosksRepository.update(kiosk);
        });
        if (botAppId) await this.botAppService.delete(botAppId, instanceId);
        return kiosk;
    };

    public verifyPin = async (instanceId: number, botAppId: number, kioskPin: string): Promise<UserDTO> => {
        const userSetting = await this.timeTrackingUserSettingService.getTimeTrackingUserSettingByKioskPin(instanceId, kioskPin);
        if (!userSetting) {
            throw new NotFoundError('User setting not found', ErrorCodes.TIME_TRACKING_KIOSK_PIN_NOT_FOUND);
        }
        const kiosk = await this.timeTrackingKiosksRepository.getByBotAppId(botAppId, instanceId);
        if (kiosk?.siteId) {
            const kioskSiteUsers = await this.getUsersBySiteId(instanceId, kiosk.siteId);
            if (!kioskSiteUsers.includes(userSetting.userId!)) {
                throw new NotFoundError('KIOSK PIN NOT FOUND', ErrorCodes.TIME_TRACKING_KIOSK_PIN_NOT_FOUND);
            }
        }
        const user = await this.usersService.getByIdOrFail(instanceId, userSetting.userId);
        await this.attachmentsService.addSignedUrlToModelWithImages(instanceId, [user], ['profilePicture']);
        return userDAOtoUserDTO(user);
    };

    private handleSiteDeletedEvent = async (params: { siteId: number; instanceId: number }) => {
        await this.timeTrackingKiosksRepository.removeKiosksFromSite(params.siteId, params.instanceId);
    };

    private getUsersBySiteId = async (instanceId: number, siteId: number): Promise<number[]> => {
        const site = await this.siteService.getById(siteId, instanceId);
        if (!site) return [];
        return site.getUsers().map((user) => user.getUserId());
    };
}
