import { BadRequestError, BaseController, CapabilityName, CapabilityType, ErrorCodes, ForbiddenError, LoggerPort } from '@humand-packages/common';
import { Request, NextFunction } from 'express';
import { NO_CONTENT, OK } from 'http-status-codes';
import { Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { OrganizationChartsService } from '../../../organizationCharts/business/services/organizationChartsService';
import { PermissionsService } from '../../../permissions/business/services/permissionsService';
import { User } from '../../../users/business/models/user';
import { UsersService } from '../../../users/business/services/usersService';
import { userDAOtoUser } from '../../../users/infrastructure/mappers/usersMapper';
import { CategorizedHourStatusOverride } from '../../business/models/timeTracking/categorizedHourStatusOverride';
import { TimeTrackingNotifyBulkCategorizedHourReviewedPort } from '../../business/ports/timeTrackingNotifyBulkCategorizedHourReviewedPort';
import { TimeTrackingHourCategorizationService } from '../../business/services/hourCategorizationService';
import { TimeTrackingCategorizedHourApprovalService } from '../../business/services/timeTracking/timeTrackingCategorizedHourApprovalService';
import { TimeTrackingCategorizedHourNotificationsService } from '../../business/services/timeTracking/timeTrackingCategorizedHourNotificationsService';
import { TimeTrackingCategorizedHourService } from '../../business/services/timeTracking/timeTrackingCategorizedHourService';
import {
    TimeTrackingCategorizedHourStatus,
    TimeTrackingCategorizedHourUpdatableStatuses,
} from '../../infrastructure/constants/timeTracking/categorizedHourStatus';
import { validateBulkReviewShape } from '../helpers/validateBulkReviewShape';
import { CategorizationExplanationSC } from '../serializationClasses/categorizationExplanationSC';
import { EditCategorizedHourStatusSC } from '../serializationClasses/editCategorizedHourStatusSC';
import { TimeTrackingCategorizedHourApprovalStepSC } from '../serializationClasses/TimeTrackingCategorizedHourApprovalStepSC';
import { TimeTrackingCategorizedHourCategoryCountsSC } from '../serializationClasses/TimeTrackingCategorizedHourCategoryCountsSC';
import { TimeTrackingCategorizedHourDetailSC } from '../serializationClasses/timeTrackingCategorizedHourDetailSC';
import { TimeTrackingCategorizedHoursSC } from '../serializationClasses/TimeTrackingCategorizedHoursSC';
import { TimeTrackingHourCategoriesSC } from '../serializationClasses/TimeTrackingHourCategoriesSC';
import { TimeTrackingResponse } from '../types/response';
import { BulkReviewCategorizedHoursVC } from '../validationClasses/hourCategorization/bulkReviewCategorizedHoursVC';
import { CategorizedHoursFiltersVC } from '../validationClasses/hourCategorization/categorizedHoursFiltersVC';
import { CreateHourCategoryVC } from '../validationClasses/hourCategorization/createHourCategoryVC';
import { EditCategorizedHourStatusVC } from '../validationClasses/hourCategorization/editCategorizedHourStatusVC';
import { ExplainCategorizationVC } from '../validationClasses/hourCategorization/explainCategorizationVC';
import { GetCategorizedHoursVC } from '../validationClasses/hourCategorization/getCategorizedHoursVC';
import { ReviewCategorizedHourApprovalStepVC } from '../validationClasses/hourCategorization/reviewCategorizedHourApprovalStepVC';
import { UpdateCategorizedHourVC } from '../validationClasses/hourCategorization/updateCategorizedHourVC';
import { UpdateHourCategoryVC } from '../validationClasses/hourCategorization/updateHourCategoryVC';
import { RecalculateCategorizedHoursVC } from '../validationClasses/timeTracking/recalculateCategorizedHoursVC';

@Service()
export class TimeTrackingHourCategorizationController extends BaseController {
    constructor(
        private readonly hourCategorizationService: TimeTrackingHourCategorizationService,
        private readonly categorizedHourApprovalService: TimeTrackingCategorizedHourApprovalService,
        private readonly categorizedHourNotificationsService: TimeTrackingCategorizedHourNotificationsService,
        private readonly permissionService: PermissionsService,
        private readonly organizationService: OrganizationChartsService,
        private readonly usersService: UsersService,
        @PortsDI(TimeTrackingNotifyBulkCategorizedHourReviewedPort)
        private readonly notifyBulkPort: TimeTrackingNotifyBulkCategorizedHourReviewedPort,
        private readonly categorizedHourService: TimeTrackingCategorizedHourService,
        logger: LoggerPort,
    ) {
        super(logger);
    }

    public listHourCategories = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination, queryParams } = res.locals;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);
            const { search } = queryParams as { search?: string };
            const result = await this.hourCategorizationService.listHourCategories(
                instanceOfLoggedUser.getId(),
                requiredLimitOffsetPagination,
                undefined,
                search,
            );
            res.json(
                new TimeTrackingHourCategoriesSC().serializePaginatedList({
                    count: result.count,
                    limit: requiredLimitOffsetPagination.limit,
                    offset: requiredLimitOffsetPagination.offset,
                    rows: result.rows,
                }),
            );
            res.status(OK).end();
        });
    };

    public getHourCategory = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, id } = res.locals;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);
            const result = await this.hourCategorizationService.getHourCategory(instanceOfLoggedUser.getId(), id);
            res.status(OK).json(result);
        });
    };

    public createHourCategory = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, bodyParams } = res.locals;
        const params = bodyParams as CreateHourCategoryVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);
            const result = await this.hourCategorizationService.createHourCategory(instanceOfLoggedUser.getId(), params);
            res.status(OK).json(result);
        });
    };

    public updateHourCategory = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, id, bodyParams } = res.locals;
        const params = bodyParams as UpdateHourCategoryVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);
            const result = await this.hourCategorizationService.updateHourCategory(instanceOfLoggedUser.getId(), id, params);
            res.status(OK).json(result);
        });
    };

    public deleteHourCategory = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, id } = res.locals;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateManageTimeTracking(user);
            await this.hourCategorizationService.deleteHourCategory(instanceOfLoggedUser.getId(), id);
            res.status(NO_CONTENT).end();
        });
    };

    public getPendingAmount = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            const result = await this.hourCategorizationService.getPendingCategorizedHoursAmount(instanceOfLoggedUser.getId(), user.getId());
            res.status(OK).json({ amount: result });
        });
    };

    public listCategorizedHours = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination, queryParams } = res.locals;
        const filters = queryParams as GetCategorizedHoursVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            await this.performCapabilityValidations(user, filters, CapabilityName.MANAGE_TIME_TRACKING);
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            const { count, rows } = await this.hourCategorizationService.listCategorizedHours(
                instanceOfLoggedUser,
                requiredLimitOffsetPagination,
                filters,
                user,
                hasManageCapability,
            );
            res.status(OK).json(
                new TimeTrackingCategorizedHoursSC().serializePaginatedList({
                    count,
                    limit: requiredLimitOffsetPagination.limit,
                    offset: requiredLimitOffsetPagination.offset,
                    rows,
                }),
            );
        });
    };

    public getCategorizedHourCategoryCounts = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, queryParams } = res.locals;
        const filters = queryParams as CategorizedHoursFiltersVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            await this.performCapabilityValidations(user, filters, CapabilityName.MANAGE_TIME_TRACKING);
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            const counts = await this.hourCategorizationService.countCategorizedHoursByCategory(
                instanceOfLoggedUser,
                filters,
                user,
                hasManageCapability,
            );
            res.status(OK).json(new TimeTrackingCategorizedHourCategoryCountsSC().serialize(counts));
        });
    };

    public explainCategorization = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, queryParams } = res.locals;
        const params = queryParams as ExplainCategorizationVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            const explanation = await this.hourCategorizationService.explainCategorization(
                instanceOfLoggedUser,
                params.userId,
                params.dateString,
                user.getId(),
                hasManageCapability,
            );
            res.status(OK).json(new CategorizationExplanationSC().serialize(explanation));
        });
    };

    public getCategorizedHour = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, categorizedHourId } = res.locals;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            const [hour, approvalSteps] = await Promise.all([
                this.hourCategorizationService.getCategorizedHour(instanceOfLoggedUser, categorizedHourId, hasManageCapability, user.getId()),
                this.categorizedHourApprovalService.getEnrichedCategorizedHourApprovalSteps(instanceOfLoggedUser.getId(), categorizedHourId),
            ]);

            res.status(OK).json(new TimeTrackingCategorizedHourDetailSC().serialize({ approvalSteps, hour }));
        });
    };

    /**
     * @deprecated Status updates will be handled exclusively by `editCategorizedHourStatus`.
     * This endpoint will be removed once all callers have migrated.
     */
    public updateCategorizedHour = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, categorizedHourId, bodyParams } = res.locals;
        const user = userDAOtoUser(loggedUser);
        const params = bodyParams as UpdateCategorizedHourVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            if (params.status === undefined && params.note === undefined) {
                throw new BadRequestError('At least one of status or note must be provided', ErrorCodes.BAD_REQUEST);
            }
            this.permissionService.validateViewTimeTracking(user);
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            if (params.status) {
                this.validateUpdatableStatus(params.status);
            }
            const result = await this.hourCategorizationService.updateCategorizedHour(
                instanceOfLoggedUser,
                categorizedHourId,
                params,
                hasManageCapability,
                user.getId(),
            );
            res.status(OK).json(result);
        });
    };

    public reviewCategorizedHourApprovalStep = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, categorizedHourId, approvalStepId, bodyParams } = res.locals;
        const user = userDAOtoUser(loggedUser);
        const params = bodyParams as ReviewCategorizedHourApprovalStepVC;
        await this.endpointHandlerWithGenerics(
            next,
            async () => {
                this.permissionService.validateViewTimeTracking(user);
                const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
                await this.categorizedHourApprovalService.reviewCategorizedHourApprovalStep(
                    user.getId(),
                    hasManageCapability,
                    instanceOfLoggedUser.getId(),
                    categorizedHourId,
                    approvalStepId,
                    { comment: params.comment, state: params.state },
                );
                const [enrichedSteps, status] = await Promise.all([
                    this.categorizedHourApprovalService.getEnrichedCategorizedHourApprovalSteps(instanceOfLoggedUser.getId(), categorizedHourId),
                    this.categorizedHourApprovalService.getCategorizedHourStatus(categorizedHourId, instanceOfLoggedUser.getId()),
                ]);
                res.status(OK).json({
                    approvalSteps: new TimeTrackingCategorizedHourApprovalStepSC().serializeList(enrichedSteps),
                    status,
                });
            },
            async () => {
                await this.categorizedHourNotificationsService.notifyCategorizedHourStepReviewed(
                    instanceOfLoggedUser.getId(),
                    categorizedHourId,
                    approvalStepId,
                );
            },
        );
    };

    public bulkReviewCategorizedHours = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, bodyParams } = res.locals;
        const user = userDAOtoUser(loggedUser);
        const params = bodyParams as BulkReviewCategorizedHoursVC;
        await this.endpointHandlerWithGenerics(
            next,
            async () => {
                this.permissionService.validateViewTimeTracking(user);
                validateBulkReviewShape(params);
                const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
                const result = await this.categorizedHourApprovalService.bulkReviewCategorizedHours(
                    instanceOfLoggedUser,
                    user,
                    params,
                    hasManageCapability,
                );
                res.status(OK).json({
                    reviewedIds: result.reviewedIds,
                    skippedCount: result.skippedCount,
                });
                return { reviewedSteps: result.reviewedSteps };
            },
            async ({ reviewedSteps }) => {
                if (reviewedSteps.length === 0) return;
                await this.notifyBulkPort.enqueue({
                    instanceId: instanceOfLoggedUser.getId(),
                    loggedUserId: user.getId(),
                    reviewedSteps,
                });
            },
        );
    };

    public editCategorizedHourStatus = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, categorizedHourId, bodyParams } = res.locals;
        const user = userDAOtoUser(loggedUser);
        const params = bodyParams as EditCategorizedHourStatusVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            if (!this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING])) {
                throw new ForbiddenError('Only admins can override the status', ErrorCodes.ACCESS_DENIED);
            }
            const result = await this.categorizedHourApprovalService.editCategorizedHourStatus(
                instanceOfLoggedUser.getId(),
                categorizedHourId,
                params.status,
                new CategorizedHourStatusOverride({ adminId: user.getId(), comment: params.comment ?? null }),
            );
            res.status(OK).json(new EditCategorizedHourStatusSC().serialize({ comment: params.comment ?? null, result, status: params.status }));
        });
    };

    public recalculateForDevops = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, bodyParams } = res.locals;
        const params = bodyParams as RecalculateCategorizedHoursVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            const result = await this.categorizedHourService.recalculateCategorizedHourDevops({
                instanceId: params.instanceId,
                categorizedHourId: params.categorizedHourId,
                triggeredByUserId: loggedUser.id,
                dryRun: params.dryRun,
            });
            res.status(OK).json(result);
        });
    };

    /**
     * Validates the capabilities of the user and based on the capability, populates the userIds filter.
     * If the user has the capability to manage time tracking, it will populate the userIds filter with all the users in the instance.
     * If the user has the capability to view time tracking, it will populate the userIds filter with the subordinate users of the user.
     * If the user provides userIds, it will validate that the user has access to the provided userIds.
     */
    private async performCapabilityValidations(user: User, params: Partial<GetCategorizedHoursVC>, capability: CapabilityType): Promise<void> {
        const capabilities = user.getGeneralPermissions() ?? {};
        const hasManagementCapability = capabilities[capability];
        if (params.userIds?.length) {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            if (!hasManagementCapability && params.userIds.some((id) => !subordinateIds.includes(id))) {
                throw new ForbiddenError('The provided user has no access to this resource', ErrorCodes.ACCESS_DENIED);
            }
        }
        if (params.search && !params.userIds?.length && !hasManagementCapability) {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            params.userIds = subordinateIds;
        }
        if (this.hasNoFiltersForCategorizedHours(params) && hasManagementCapability) {
            params.userIds = await this.usersService.getUserIdsInInstance(user.getInstanceId());
        }
        if (this.hasNoFiltersForCategorizedHours(params) && !hasManagementCapability) {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            params.userIds = subordinateIds;
        }
    }

    private hasNoFiltersForCategorizedHours(params: Partial<GetCategorizedHoursVC>): boolean {
        return !params.search && !params.userIds?.length;
    }

    private validateUpdatableStatus(status: TimeTrackingCategorizedHourStatus): void {
        if (!TimeTrackingCategorizedHourUpdatableStatuses.includes(status)) {
            throw new BadRequestError('Invalid status', ErrorCodes.BAD_REQUEST);
        }
    }
}
