import {
    BadRequestError,
    BaseController,
    BaseXlsxPort,
    DeferredExecutionQueuePort,
    ENV_OPTIONAL_NUMBER_KEYS_MAPPING,
    ErrorCodes,
    ISO_DATE,
    LoggerPort,
    moment,
    QueueMessageTypes,
    QUEUES,
} from '@humand-packages/common';
import { NextFunction, Request } from 'express';
import { ACCEPTED, CREATED, NO_CONTENT, OK } from 'http-status-codes';
import { Inject, Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { DEFERRED_JOBS_STORAGE_FOLDER, JobsService } from '../../../../services/jobs';
import { ReportTemplatesService } from '../../../../services/reports/reportTemplates';
import { StorageService } from '../../../../services/storage';
import { DeprecatedHumandResponse } from '../../../../types/locals';
import { JobTypes } from '../../../jobs/business/constants/jobTypes';
import { PermissionsService } from '../../../permissions/business/services/permissionsService';
import { GROUP_MEMBER_POLICIES } from '../../../regions/business/types/regions';
import { User } from '../../../users/business/models/user';
import { UsersService } from '../../../users/business/services/usersService';
import { UserDAO } from '../../../users/infrastructure/dataAccessObjects/userDAO';
import { userDAOtoUser } from '../../../users/infrastructure/mappers/usersMapper';
import { WorkScheduleCreationParameters, WorkScheduleUpdateAttributes } from '../../business/models/workSchedule';
import WorkScheduleServicePort from '../../business/ports/workScheduleServicePort';
import { WorkScheduleSC } from '../serializationClasses/workScheduleSC';
import { WorkScheduleUserAssigmentSC } from '../serializationClasses/workScheduleUserAssigmentSC';
import { WorkSchedulesResponse } from '../types/response';
import { AssignUserWorkScheduleVC } from '../validationClasses/assignUserWorkScheduleVC';
import { BulkUpdateWorkScheduleVC } from '../validationClasses/bulkUpdateWorkScheduleVC';
import { CreateWorkScheduleVC } from '../validationClasses/createScheduleVC';
import { GetWorkSchedulesRequestsVC } from '../validationClasses/getWorkScheduleRequestVC';
import { ScheduledDayVC } from '../validationClasses/scheduledDayVC';
import { TimeSlotVC } from '../validationClasses/timeSlotVC';
import { UpdateWorkScheduleVC } from '../validationClasses/updateScheduleVC';
import { WorkScheduleUserAssignmentFiltersVC } from '../validationClasses/workScheduleUserAssignmentFiltersVC';

const DEFAULT_MAX_WORKDAYS_USERS_MIGRATION = 1000;
@Service()
export class WorkScheduleController extends BaseController {
    constructor(
        @PortsDI(WorkScheduleServicePort) private readonly workScheduleService: WorkScheduleServicePort,
        private readonly permissionService: PermissionsService,
        private readonly usersService: UsersService,
        private readonly xlsxPort: BaseXlsxPort,
        private readonly jobsService: JobsService,
        private readonly reportTemplatesService: ReportTemplatesService,
        private readonly storageService: StorageService,
        private readonly queuePort: DeferredExecutionQueuePort,
        @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.NOTIFICATION_DELAY) notificationDelay: number,
        logger: LoggerPort,
    ) {
        super(logger, notificationDelay);
    }

    @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.MAX_BULK_UPDATE_WORK_SCHEDULES_RECORDS)
    private maxBulkUpdateWorkScheduleRecords!: number;

    public createSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { bodyParams, instanceOfLoggedUser, loggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        const params = bodyParams as CreateWorkScheduleVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            this.checkScheduleSlotsValidity(params.scheduledDays, params.generalTimeSlots);
            this.validateScheduledDays(params.scheduledDays);
            this.validateTimeRange(params.startDate, params.endDate);
            this.validateTimeSlots(params.scheduledDays, params.generalTimeSlots);
            if (params.userMode === GROUP_MEMBER_POLICIES.ALL_USERS) {
                params.collaborators = await this.usersService.getUserIdsInInstance((user as User).getInstanceId());
            } else if (params.userMode === GROUP_MEMBER_POLICIES.NONE) {
                params.collaborators = [];
            }
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const creationAttributes = this.fillCreationAttributes(user, params);
            const createdSchedule = await this.workScheduleService.createSchedule(creationAttributes);
            const timezone = instanceOfLoggedUser.getTimezone();
            await this.workScheduleService.assignUsersToSchedule(
                {
                    collaborators: params.collaborators!,
                    endDate: params.endDate,
                    id: createdSchedule.id,
                    instanceId: (user as User).instanceId,
                    scheduledDays: params.scheduledDays.map((day: ScheduledDayVC) => day.dayOfWeek),
                    startDate: params.startDate ?? moment.tz(timezone).format(ISO_DATE),
                },
                timezone,
            );
            res.json(createdSchedule).status(CREATED).end();
        });
    };

    public listSchedules = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { loggedUser, requiredLimitOffsetPagination } = res.locals;
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const params = res.locals.queryParams as GetWorkSchedulesRequestsVC;
            const schedules = await this.workScheduleService.listWorkSchedules(
                { ...params, instanceId: loggedUser.instanceId },
                requiredLimitOffsetPagination,
            );
            res.json(
                new WorkScheduleSC().serializePaginatedList({
                    count: schedules.count,
                    limit: requiredLimitOffsetPagination.limit,
                    offset: requiredLimitOffsetPagination.offset,
                    rows: schedules.rows,
                }),
            ).end();
        });
    };

    public assignUserToWorkSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, id, instanceOfLoggedUser } = res.locals;
        const params = res.locals.queryParams as AssignUserWorkScheduleVC;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            const schedule = await this.workScheduleService.assignWorkScheduleToUser(
                id,
                params.workScheduleId,
                (user as User).instanceId,
                instanceOfLoggedUser.getTimezone(),
            );
            res.json(schedule).status(OK).end();
        });
    };

    public unassignUserFromWorkSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, id, instanceOfLoggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            await this.workScheduleService.unassignWorkScheduleFromUser(id, (user as User).instanceId, instanceOfLoggedUser.getTimezone());
            res.status(NO_CONTENT).end();
        });
    };

    public getSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { loggedUser, id, instanceOfLoggedUser } = res.locals;
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const schedule = await this.workScheduleService.getWorkSchedule(id, loggedUser.instanceId, instanceOfLoggedUser.getTimezone());
            res.json(schedule).end();
        });
    };

    public eliminateSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { loggedUser, id } = res.locals;
            this.permissionService.validateManageWorkSchedules(loggedUser);
            await this.workScheduleService.eliminateWorkSchedule(id, loggedUser.instanceId);
            res.status(NO_CONTENT).end();
        });
    };

    public getScheduleByUser = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { instanceOfLoggedUser, loggedUser } = res.locals;
            const schedule = await this.workScheduleService.getWorkScheduleByUser(
                loggedUser.id,
                loggedUser.instanceId,
                instanceOfLoggedUser.getTimezone(),
            );
            res.json(schedule).end();
        });
    };

    public getScheduleByUserId = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { id, instanceOfLoggedUser, loggedUser } = res.locals;
            const schedule = await this.workScheduleService.getWorkScheduleByUser(id, loggedUser.instanceId, instanceOfLoggedUser.getTimezone());
            res.json(schedule).end();
        });
    };

    public updateSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { bodyParams, loggedUser, id, instanceOfLoggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        const params = bodyParams as UpdateWorkScheduleVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            let updateSchedule = false;
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const newScheduleAttributes: WorkScheduleUpdateAttributes = {};
            if (params.scheduledDays) {
                this.checkScheduleSlotsValidity(params.scheduledDays, params.generalTimeSlots);
                this.validateScheduledDays(params.scheduledDays);
                this.validateTimeSlots(params.scheduledDays, params.generalTimeSlots);
                newScheduleAttributes.scheduledDays = params.scheduledDays;
                if (params.generalTimeSlots) newScheduleAttributes.generalTimeSlots = params.generalTimeSlots;
                updateSchedule = true;
            }
            if (params.userMode === GROUP_MEMBER_POLICIES.ALL_USERS) {
                params.collaborators = await this.usersService.getUserIdsInInstance((user as User).getInstanceId());
            } else if (params.userMode === GROUP_MEMBER_POLICIES.NONE) {
                params.collaborators = [];
            }
            if (params.name != null) {
                newScheduleAttributes.name = params.name;
                updateSchedule = true;
            }
            if (params.description != null) {
                newScheduleAttributes.description = params.description;
                updateSchedule = true;
            }
            let updatedSchedule;
            if (updateSchedule) {
                updatedSchedule = await this.workScheduleService.updateWorkSchedule(
                    (user as User).instanceId,
                    id,
                    newScheduleAttributes,
                    instanceOfLoggedUser.getTimezone(),
                );
            }
            if (params.collaborators) {
                if (!updatedSchedule)
                    updatedSchedule = await this.workScheduleService.getWorkSchedule(
                        id,
                        (user as User).instanceId,
                        instanceOfLoggedUser.getTimezone(),
                    );
                const timezone = instanceOfLoggedUser.getTimezone();
                await this.workScheduleService.assignUsersToSchedule(
                    {
                        collaborators: params.collaborators,
                        endDate: params.endDate,
                        id,
                        instanceId: (user as User).instanceId,
                        scheduledDays: updatedSchedule.scheduledDays.map((day: ScheduledDayVC) => day.dayOfWeek),
                        startDate: params.startDate ?? moment.tz(timezone).format(ISO_DATE),
                    },
                    timezone,
                );
            }
            res.json(updatedSchedule).status(OK).end();
        });
    };

    public bulkUpdateWorkSchedulesTemplate = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const reportXlsxFile = this.reportTemplatesService.generateBulkUpdateWorkSchedulesTemplate(user);
            await this.writeExcelReportToRes(res, reportXlsxFile);
            res.end();
        });
    };

    public bulkUpdateWorkSchedules = async ({ file }: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { bodyParams, loggedUser } = res.locals;
        const params = bodyParams as BulkUpdateWorkScheduleVC;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            if (!file) throw new BadRequestError('file is necessary', ErrorCodes.INVALID_DATA);
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const workdaysData = await this.workScheduleService.getWorkSchedulesByXlsxBuffer(file.buffer, user);
            const job = await this.jobsService.create(false, JobTypes.BULK_UPDATE_WORK_SCHEDULES, loggedUser, file.originalname, workdaysData.length);
            const filename = `${job.id.toString()}-${file!.originalname}`;
            // TODO: migrate to jobsService.attachUploadedFileToJob to centralize the upload+persist of fileKey on Jobs
            const fileKey = await this.storageService.uploadBuffer(filename, file!.buffer, DEFERRED_JOBS_STORAGE_FOLDER);
            if (params.override) {
                await this.workScheduleService.deleteExistingWorkSchedules(loggedUser.instanceId);
            }
            await this.queuePort.sendMessage(
                QUEUES.WORKER,
                QueueMessageTypes.BULK_UPDATE_WORK_SCHEDULES,
                {
                    fileKey,
                    instanceId: loggedUser.getInstanceId(),
                    jobId: job.id,
                    loggedUserId: loggedUser.id,
                },
                {
                    messageGroupId: loggedUser.instanceId.toString(),
                },
            );
            res.status(ACCEPTED).end();
        });
    };

    public migrateWorkdaysToWorkSchedules = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, limitOffsetPagination } = res.locals;
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateDevOps(loggedUser);
            const result = await this.workScheduleService.migrateWorkdaysToWorkSchedules(
                userDAOtoUser(loggedUser as UserDAO),
                limitOffsetPagination.limit ?? DEFAULT_MAX_WORKDAYS_USERS_MIGRATION,
            );
            res.json(result);
            res.status(Object.keys(result).length !== 0 ? CREATED : OK).end();
        });
    };

    private fillCreationAttributes(user: User, params: CreateWorkScheduleVC): WorkScheduleCreationParameters {
        return {
            createdBy: user.id,
            description: params.description ?? '',
            generalTimeSlots: params.generalTimeSlots,
            instanceId: user.instanceId,
            name: params.name ?? '',
            scheduledDays: params.scheduledDays,
        };
    }

    private validateTimeRange(startDate: string | undefined, endDate: string | undefined) {
        if ((startDate && endDate && moment(startDate).isAfter(moment(endDate))) || (!startDate && endDate)) {
            throw new BadRequestError('startDate must be before endDate', ErrorCodes.WORK_SCHEDULE_INVALID_TIME_RANGE);
        }
    }

    private checkScheduleSlotsValidity(scheduledDays: ScheduledDayVC[], generalTimeSlots: TimeSlotVC[] | undefined) {
        if (generalTimeSlots && scheduledDays.some((scheduleDay) => scheduleDay.timeSlots.length > 0)) {
            throw new BadRequestError(
                'You cannot have both generalTimeSlots and scheduledDays with slots',
                ErrorCodes.WORK_SCHEDULE_INVALID_TIME_SLOTS,
            );
        }
    }

    private validateTimeSlots(scheduledDays: ScheduledDayVC[], generalTimeSlots: TimeSlotVC[] | undefined) {
        scheduledDays.forEach((day) => {
            day.timeSlots.forEach(this.validateTimeSlot);
        });
        generalTimeSlots?.forEach(this.validateTimeSlot);
    }

    private validateTimeSlot(timeSlot: TimeSlotVC) {
        timeSlot.startTime = timeSlot.startTime.trim();
        timeSlot.endTime = timeSlot.endTime.trim();
        if (timeSlot.endTime == timeSlot.startTime) {
            throw new BadRequestError('starTime and endTime must be not equals', ErrorCodes.WORK_SCHEDULE_INVALID_TIME_RANGE);
        }
    }

    private validateScheduledDays(scheduledDays: ScheduledDayVC[] | undefined) {
        const uniqueDays = new Set(scheduledDays?.map((scheduledDay: ScheduledDayVC) => scheduledDay.dayOfWeek));
        if (uniqueDays.size != scheduledDays?.length) {
            throw new BadRequestError('You cant have repeated days in a schedule', ErrorCodes.WORK_SCHEDULE_INVALID_DAYS);
        }
    }

    public getAllUsersAssignedToWorkSchedule = async (_req: Request, res: WorkSchedulesResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { loggedUser, id, instanceOfLoggedUser, requiredLimitOffsetPagination, queryParams } = res.locals;
            const filters = queryParams as WorkScheduleUserAssignmentFiltersVC;
            const user = userDAOtoUser(loggedUser);
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const userAssigments = await this.workScheduleService.getAllUsersAssignedToWorkSchedule(
                id,
                loggedUser.instanceId,
                user,
                requiredLimitOffsetPagination,
                instanceOfLoggedUser.getTimezone(),
                filters,
            );
            res.json(
                new WorkScheduleUserAssigmentSC().serializePaginatedList({
                    count: userAssigments.count,
                    limit: requiredLimitOffsetPagination.limit,
                    offset: requiredLimitOffsetPagination.offset,
                    rows: userAssigments.rows,
                }),
            ).end();
        });
    };

    public getAllUserAssignmentsInWorkSchedule = async (_req: Request, res: WorkSchedulesResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { loggedUser, id, instanceOfLoggedUser, userId, requiredLimitOffsetPagination } = res.locals;
            this.permissionService.validateManageWorkSchedules(loggedUser);
            const userAssigments = await this.workScheduleService.getAllUserAssignmentsInWorkSchedule(
                id,
                instanceOfLoggedUser.getId(),
                userId,
                requiredLimitOffsetPagination,
                instanceOfLoggedUser.getTimezone(),
            );
            res.json(
                new WorkScheduleUserAssigmentSC().serializePaginatedList({
                    count: userAssigments.count,
                    limit: requiredLimitOffsetPagination.limit,
                    offset: requiredLimitOffsetPagination.offset,
                    rows: userAssigments.rows,
                }),
            ).end();
        });
    };

    public deleteWorkScheduleUser = async (_req: Request, res: WorkSchedulesResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const { loggedUser, id, workScheduleId } = res.locals;
            this.permissionService.validateManageWorkSchedules(loggedUser);
            await this.workScheduleService.deleteWorkScheduleUser(id, workScheduleId, loggedUser.instanceId);
            res.status(NO_CONTENT).end();
        });
    };
}
