import { randomBytes } from 'crypto';
import { CapabilityName, ForbiddenError, NotFoundError, ErrorCodes, GoneError, InternalServerError, BaseAdapter } from '@humand-packages/common';
import moment from 'moment';
import { Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { UserBotApp } from '../../../botApps/business/models/botApp';
import UserBotAppService from '../../../botApps/business/services/botApp';
import UserBotAppPermissionService from '../../../botApps/business/services/botAppPermission';
import { UserInterface } from '../../../users/business/models/userInterface';
import TimeTrackingKioskLinkRepositoryPort from '../../infrastructure/ports/TimeTrackingKioskLinkRepositoryPort';
import TimeTrackingKioskRepositoryPort from '../../infrastructure/ports/TimeTrackingKioskRepositoryPort';
import { TimeTrackingKiosk } from '../models/timeTrackingKiosk';
import { TimeTrackingKioskLink, TimeTrackingKioskLinkAcceptedData, TimeTrackingKioskLinkForCreation } from '../models/timeTrackingKioskLink';
import { TimeTrackingKioskLinkPort } from '../ports/timeTrackingKioskLinkPort';
import { TimeTrackingKioskLinkStatus } from '../types/kioskLinkStatus';
import { TimeTrackingKioskStatus } from '../types/kioskStatus';

const MAX_ATTEMPTS = 10;
const EXPIRATION_MINUTES = 10;

@Service()
export default class TimeTrackingKioskLinkAdapter extends BaseAdapter implements TimeTrackingKioskLinkPort {
    constructor(
        @PortsDI(TimeTrackingKioskLinkRepositoryPort) private readonly timeTrackingKioskLinkRepository: TimeTrackingKioskLinkRepositoryPort,
        @PortsDI(TimeTrackingKioskRepositoryPort) private readonly timeTrackingKioskRepository: TimeTrackingKioskRepositoryPort,
        private readonly botAppService: UserBotAppService,
        private readonly botAppPermissionsService: UserBotAppPermissionService,
    ) {
        super();
    }

    public create = async (): Promise<TimeTrackingKioskLink> => {
        const code = await this.generateCode();
        const expirationDate = moment().add(EXPIRATION_MINUTES, 'minutes').toDate();
        const link = new TimeTrackingKioskLinkForCreation({
            code,
            expirationDate,
            expirationInMinutes: EXPIRATION_MINUTES,
            status: TimeTrackingKioskLinkStatus.ACTIVE,
        });
        return await this.timeTrackingKioskLinkRepository.create(link);
    };

    public linkCode = async (kioskId: number, user: UserInterface, code: string): Promise<TimeTrackingKiosk> => {
        return await this.timeTrackingKioskRepository.getTransaction(async () => {
            const kiosk = await this.timeTrackingKioskRepository.getByIdOrFail(user.instanceId, kioskId);
            if (kiosk.status === TimeTrackingKioskStatus.ACTIVE) {
                throw new ForbiddenError('Kiosk is linked already', ErrorCodes.TIME_TRACKING_KIOSK_LINK_CODE_ERROR);
            }
            const links = await this.timeTrackingKioskLinkRepository.findActiveLinksByCode(code);
            if (links.length) {
                const link = links[0];
                if (this.isLinkExpired(link)) {
                    //If the first link is expired, all the links are expired, because the list is ordered by expiration date desc
                    await this.timeTrackingKioskLinkRepository.updateLinksStatus(
                        TimeTrackingKioskLinkStatus.EXPIRED,
                        links.map((link) => link.id),
                    );
                    throw new GoneError('The code has expired', ErrorCodes.TIME_TRACKING_KIOSK_LINK_EXPIRED_ERROR);
                } else {
                    link.instanceId = user.instanceId;
                    link.kioskId = kiosk.id;
                    link.status = TimeTrackingKioskLinkStatus.WAITING_FOR_LINK;
                    await this.timeTrackingKioskLinkRepository.update(link);
                    kiosk.status = TimeTrackingKioskStatus.WAITING_FOR_LINK;
                    await this.timeTrackingKioskRepository.update(kiosk);
                    if (links.length > 1) {
                        //If there are others links, mark as expired
                        await this.timeTrackingKioskLinkRepository.updateLinksStatus(
                            TimeTrackingKioskLinkStatus.EXPIRED,
                            links.slice(1).map((link) => link.id),
                        );
                    }
                    return kiosk;
                }
            } else {
                throw new NotFoundError('Code not found', ErrorCodes.NOT_FOUND);
            }
        });
    };

    public validateCode = async (id: number, code: string): Promise<TimeTrackingKioskLinkAcceptedData> => {
        return await this.timeTrackingKioskLinkRepository.getTransaction(async () => {
            const link = await this.timeTrackingKioskLinkRepository.findByIdAndCode(id, code);
            if (link.status === TimeTrackingKioskLinkStatus.EXPIRED) {
                throw new GoneError('Code has expired', ErrorCodes.TIME_TRACKING_KIOSK_LINK_EXPIRED_ERROR);
            }
            if (this.isLinkExpired(link)) {
                await this.timeTrackingKioskLinkRepository.updateLinksStatus(TimeTrackingKioskLinkStatus.EXPIRED, [link.id]);
                throw new GoneError('Code has expired', ErrorCodes.TIME_TRACKING_KIOSK_LINK_EXPIRED_ERROR);
            }
            if (!link.kioskId || !link.instanceId) {
                throw new ForbiddenError('Code is not linked yet', ErrorCodes.TIME_TRACKING_KIOSK_LINK_CODE_NOT_LINKED_ERROR);
            }
            if (link.status === TimeTrackingKioskLinkStatus.USED) {
                throw new GoneError(
                    'Provided code is already active and cannot be reused.',
                    ErrorCodes.TIME_TRACKING_KIOSK_LINK_CODE_ALREADY_IN_USE_LINKED_ERROR,
                );
            }
            const kiosk = await this.timeTrackingKioskRepository.getByIdIncludeInstance(link.kioskId);
            if (!kiosk) {
                throw new NotFoundError('Kiosk not found', ErrorCodes.NOT_FOUND);
            }
            if (!kiosk.createdBy) {
                throw new NotFoundError('Created User not found', ErrorCodes.NOT_FOUND);
            }

            await this.timeTrackingKioskLinkRepository.updateLinksStatus(TimeTrackingKioskLinkStatus.USED, [link.id]);

            const botApp = await this.createKioskBotApp(kiosk.id, kiosk.createdBy, kiosk.instanceId);
            kiosk.status = TimeTrackingKioskStatus.ACTIVE;
            kiosk.botAppId = botApp.id;
            const updated = await this.timeTrackingKioskRepository.update(kiosk);

            return new TimeTrackingKioskLinkAcceptedData({
                clientId: botApp.clientId,
                clientSecret: botApp.clientSecret,
                instance: kiosk.instance,
                kiosk: updated,
            });
        });
    };

    public expireWaitingLink = async (kioskId: number, instanceId: number): Promise<void> => {
        const link = await this.timeTrackingKioskLinkRepository.findWaitingLinkByKioskId(kioskId, instanceId);
        if (link) await this.timeTrackingKioskLinkRepository.updateLinksStatus(TimeTrackingKioskLinkStatus.EXPIRED, [link.id]);
    };

    private createKioskBotApp = async (kioskId: number, userId: number, instanceId: number): Promise<UserBotApp> => {
        const botApp = await this.botAppService.create(userId, instanceId, {
            name: `kiosk-${kioskId}`,
        });
        await this.botAppPermissionsService.create({
            botAppId: botApp.id,
            capabilityName: CapabilityName.VIEW_TIME_TRACKING,
        });
        return botApp;
    };

    private isLinkExpired = (link: TimeTrackingKioskLink): boolean => {
        if (!link.expirationDate) {
            return false;
        }
        return moment().isAfter(link.expirationDate);
    };

    private generateCode = async (): Promise<string> => {
        let code: string;
        let alreadyExistingLink;
        let attempts = 0;
        do {
            if (attempts >= MAX_ATTEMPTS) {
                throw new InternalServerError(
                    'Failed to generate a unique code after maximum attempts',
                    ErrorCodes.TIME_TRACKING_KIOSK_LINK_CODE_ERROR,
                );
            }
            code = this.generateSecureRandomCode(12);
            alreadyExistingLink = await this.timeTrackingKioskLinkRepository.findActiveNonExpiredLinkByCode(code);
            attempts++;
        } while (alreadyExistingLink);
        return code;
    };

    private generateSecureRandomCode = (length: 12): string => {
        const bytes = randomBytes(length);
        const digits = Array.from(bytes).map((byte) => byte % 10);
        return digits.join('').slice(0, length);
    };
}
