import { ENV_OPTIONAL_NUMBER_KEYS_MAPPING, NotFoundError, ErrorCodes, SEQUELIZE_INSTANCE_TOKEN } from '@humand-packages/common';
import { Op } from 'sequelize';
import { Sequelize } from 'sequelize-typescript';
import { Inject, Service } from 'typedi';
import { v7 as uuidv7 } from 'uuid';
import { BaseRepositoryAdapter } from '../../../../../../dbAdapters/baseRepositoryAdapter';
import {
    TimeTrackingCategorizedHourApprovalStep,
    TimeTrackingCategorizedHourApprovalStepForCreation,
} from '../../../../business/models/timeTracking/timeTrackingCategorizedHourApprovalStep';
import {
    TimeTrackingCategorizedHourApprovalStepPotentialApprover,
    TimeTrackingCategorizedHourApprovalStepPotentialApproverForCreation,
} from '../../../../business/models/timeTracking/timeTrackingCategorizedHourApprovalStepPotentialApprover';
import { TimeTrackingCategorizedHourApprovalStepState } from '../../../constants/timeTracking/categorizedHourApprovalStepStates';
import { TimeTrackingCategorizedHourStatus } from '../../../constants/timeTracking/categorizedHourStatus';
import { TimeTrackingCategorizedHourDAO } from '../../../dataAccessObjects/hourCategory/categorizedHour';
import { TimeTrackingCategorizedHourApprovalStepDAO } from '../../../dataAccessObjects/timeTracking/timeTrackingCategorizedHourApprovalStepDAO';
import { TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO } from '../../../dataAccessObjects/timeTracking/timeTrackingCategorizedHourApprovalStepPotentialApproverDAO';
import {
    timeTrackingCategorizedHourApprovalStepDAOToModel,
    timeTrackingCategorizedHourApprovalStepToDAO,
} from '../../../mappers/timeTracking/timeTrackingCategorizedHourApprovalStepMapper';
import { timeTrackingCategorizedHourApprovalStepPotentialApproverDAOToModel } from '../../../mappers/timeTracking/timeTrackingCategorizedHourApprovalStepPotentialApproverMapper';
import { TimeTrackingCategorizedHourApprovalStepRepositoryPort } from '../../../ports/timeTracking/timeTrackingCategorizedHourApprovalStepRepositoryPort';

@Service()
export class TimeTrackingCategorizedHourApprovalStepRepositoryAdapter
    extends BaseRepositoryAdapter
    implements TimeTrackingCategorizedHourApprovalStepRepositoryPort
{
    constructor(
        @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.BULK_CREATE_CHUNK_SIZE) bulkCreateChunkSize: number,
        @Inject(SEQUELIZE_INSTANCE_TOKEN) sequelize: Sequelize,
    ) {
        super(bulkCreateChunkSize, sequelize);
    }

    public bulkCreateCategorizedHourApprovalSteps = async (
        steps: TimeTrackingCategorizedHourApprovalStepForCreation[],
    ): Promise<TimeTrackingCategorizedHourApprovalStep[]> => {
        if (steps.length === 0) return [];
        const daos = await TimeTrackingCategorizedHourApprovalStepDAO.bulkCreate(steps.map((step) => ({ ...step, id: uuidv7() })));
        return daos.map(timeTrackingCategorizedHourApprovalStepDAOToModel);
    };

    public bulkCreateCategorizedHourApprovalStepPotentialApprovers = async (
        items: TimeTrackingCategorizedHourApprovalStepPotentialApproverForCreation[],
    ): Promise<TimeTrackingCategorizedHourApprovalStepPotentialApprover[]> => {
        if (items.length === 0) return [];
        const daos = await TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO.bulkCreate(items.map((item) => ({ ...item, id: uuidv7() })));
        return daos.map(timeTrackingCategorizedHourApprovalStepPotentialApproverDAOToModel);
    };

    public listCategorizedHourApprovalSteps = async (
        categorizedHourId: string,
        instanceId: number,
    ): Promise<TimeTrackingCategorizedHourApprovalStep[]> => {
        const daos = await TimeTrackingCategorizedHourApprovalStepDAO.findAll({
            include: [
                { as: 'potentialApprovers', model: TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO },
                { as: 'categorizedHour', model: TimeTrackingCategorizedHourDAO, required: true, where: { instanceId } },
            ],
            order: [['position', 'ASC']],
            where: { categorizedHourId },
        });
        return daos.map(timeTrackingCategorizedHourApprovalStepDAOToModel);
    };

    public findCategorizedHourApprovalStep = async (
        stepId: string,
        categorizedHourId: string,
        instanceId: number,
    ): Promise<TimeTrackingCategorizedHourApprovalStep> => {
        const dao = await TimeTrackingCategorizedHourApprovalStepDAO.findOne({
            include: [
                { as: 'potentialApprovers', model: TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO },
                { as: 'categorizedHour', model: TimeTrackingCategorizedHourDAO, required: true, where: { instanceId } },
            ],
            where: { categorizedHourId, id: stepId },
        });
        if (!dao) throw new NotFoundError('Approval step not found', ErrorCodes.NOT_FOUND);
        return timeTrackingCategorizedHourApprovalStepDAOToModel(dao);
    };

    public findCategorizedHourApprovalStepForApprover = async (
        stepId: string,
        categorizedHourId: string,
        userId: number,
    ): Promise<TimeTrackingCategorizedHourApprovalStep | null> => {
        // Check authorization: step exists and userId is a potential approver
        const dao = await TimeTrackingCategorizedHourApprovalStepDAO.findOne({
            include: [
                {
                    as: 'potentialApprovers',
                    model: TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO,
                    required: true,
                    where: { userId },
                },
            ],
            where: {
                categorizedHourId,
                id: stepId,
                state: {
                    [Op.in]: [
                        TimeTrackingCategorizedHourApprovalStepState.PENDING,
                        TimeTrackingCategorizedHourApprovalStepState.APPROVED,
                        TimeTrackingCategorizedHourApprovalStepState.REJECTED,
                    ],
                },
            },
        });
        if (!dao) return null;

        // Reload all potential approvers unfiltered so the returned model is complete
        dao.potentialApprovers = await TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO.findAll({
            where: { approvalStepId: stepId },
        });

        return timeTrackingCategorizedHourApprovalStepDAOToModel(dao);
    };

    public updateCategorizedHourApprovalStep = async (
        step: TimeTrackingCategorizedHourApprovalStep,
    ): Promise<TimeTrackingCategorizedHourApprovalStep> => {
        const dao = timeTrackingCategorizedHourApprovalStepToDAO(step);
        await TimeTrackingCategorizedHourApprovalStepDAO.update(
            {
                approverId: dao.approverId,
                comment: dao.comment,
                resolutionDate: dao.resolutionDate,
                state: dao.state,
            },
            { where: { id: step.id } },
        );
        return step;
    };

    public getCategorizedHourIdsForApprover = async (userId: number, instanceId: number): Promise<string[]> => {
        const daos = await TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO.findAll({
            attributes: ['categorizedHourId'],
            include: [
                {
                    as: 'approvalStep',
                    attributes: [],
                    model: TimeTrackingCategorizedHourApprovalStepDAO,
                    required: true,
                    where: {
                        state: {
                            [Op.in]: [
                                TimeTrackingCategorizedHourApprovalStepState.PENDING,
                                TimeTrackingCategorizedHourApprovalStepState.APPROVED,
                                TimeTrackingCategorizedHourApprovalStepState.REJECTED,
                            ],
                        },
                    },
                },
                { as: 'categorizedHour', attributes: [], model: TimeTrackingCategorizedHourDAO, required: true, where: { instanceId } },
            ],
            where: { userId },
        });
        return [...new Set(daos.map((dao) => dao.categorizedHourId))];
    };

    public getStepIdsWhereUserIsPotentialApprover = async (userId: number, stepIds: string[]): Promise<string[]> => {
        if (stepIds.length === 0) return [];
        const daos = await TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO.findAll({
            attributes: ['approvalStepId'],
            where: { userId, approvalStepId: { [Op.in]: stepIds } },
        });
        return [...new Set(daos.map((dao) => dao.approvalStepId))];
    };

    public getCurrentStepIdsByCategorizedHourIds = async (categorizedHourIds: string[]): Promise<Map<string, string>> => {
        if (categorizedHourIds.length === 0) return new Map();
        const daos = await TimeTrackingCategorizedHourApprovalStepDAO.findAll({
            attributes: ['id', 'categorizedHourId'],
            where: {
                categorizedHourId: { [Op.in]: categorizedHourIds },
                state: TimeTrackingCategorizedHourApprovalStepState.PENDING,
            },
        });
        return new Map(daos.map((dao) => [dao.categorizedHourId, dao.id]));
    };

    public deleteApprovalDataByCategorizedHourIds = async (categorizedHourIds: string[]): Promise<void> => {
        if (categorizedHourIds.length === 0) return;
        await TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO.destroy({
            where: { categorizedHourId: { [Op.in]: categorizedHourIds } },
        });
        await TimeTrackingCategorizedHourApprovalStepDAO.destroy({
            where: { categorizedHourId: { [Op.in]: categorizedHourIds } },
        });
    };

    public classifyForBulkReview = async (input: {
        instanceId: number;
        hourIds: string[];
        loggedUserId: number;
    }): Promise<{
        toReview: Array<{ hourId: string; stepId: string; position: number }>;
        skippedCount: number;
    }> => {
        const { instanceId, hourIds, loggedUserId } = input;
        if (hourIds.length === 0) return { toReview: [], skippedCount: 0 };

        const [hourDAOs, stepDAOs] = await Promise.all([
            TimeTrackingCategorizedHourDAO.findAll({
                attributes: ['id', 'status', 'statusOverriddenByAdminId'],
                where: { id: { [Op.in]: hourIds }, instanceId },
            }),
            TimeTrackingCategorizedHourApprovalStepDAO.findAll({
                attributes: ['id', 'categorizedHourId', 'position'],
                include: [
                    {
                        as: 'potentialApprovers',
                        attributes: ['id'],
                        model: TimeTrackingCategorizedHourApprovalStepPotentialApproverDAO,
                        required: false,
                        where: { userId: loggedUserId },
                    },
                ],
                where: {
                    categorizedHourId: { [Op.in]: hourIds },
                    state: TimeTrackingCategorizedHourApprovalStepState.PENDING,
                },
            }),
        ]);

        const hourById = new Map(hourDAOs.map((h) => [h.id, h]));
        const stepByHourId = new Map<string, TimeTrackingCategorizedHourApprovalStepDAO>();
        for (const step of stepDAOs) stepByHourId.set(step.categorizedHourId, step);

        const toReview: Array<{ hourId: string; stepId: string; position: number }> = [];
        let skippedCount = 0;

        for (const id of hourIds) {
            const hour = hourById.get(id);
            // Skip when the hour is frozen by an admin REJECTION override. An APPROVED override
            // (admin "undo revoke") leaves the flow editable, matching reviewCategorizedHourApprovalStep.
            const isFrozen = hour != null && hour.statusOverriddenByAdminId != null && hour.status === TimeTrackingCategorizedHourStatus.REJECTED;
            if (!hour || isFrozen) {
                skippedCount++;
                continue;
            }
            const step = stepByHourId.get(id);
            if (!step || !step.potentialApprovers || step.potentialApprovers.length === 0) {
                skippedCount++;
                continue;
            }
            toReview.push({ hourId: id, stepId: step.id, position: step.position });
        }

        return { toReview, skippedCount };
    };

    public executeBulkReview = async (input: {
        stepIds: string[];
        approverId: number;
        state: TimeTrackingCategorizedHourApprovalStepState.APPROVED | TimeTrackingCategorizedHourApprovalStepState.REJECTED;
        comment: string | null;
    }): Promise<{
        reviewed: Array<{ stepId: string; hourId: string }>;
        concurrencyLosers: string[];
    }> => {
        const { stepIds, approverId, state, comment } = input;
        if (stepIds.length === 0) return { reviewed: [], concurrencyLosers: [] };

        // Optimistically review the current steps (guarded by state = PENDING).
        // Hour-status advancement is handled per-hour in the service via advanceApprovalFlowAfterReview.
        const [, returnedRows] = await TimeTrackingCategorizedHourApprovalStepDAO.update(
            { state, approverId, comment, resolutionDate: new Date() },
            {
                returning: ['id', 'categorizedHourId'],
                where: {
                    id: { [Op.in]: stepIds },
                    state: TimeTrackingCategorizedHourApprovalStepState.PENDING,
                },
            },
        );

        const reviewed = returnedRows.map((r) => ({ stepId: r.id, hourId: r.categorizedHourId }));
        const returnedStepIds = new Set(returnedRows.map((r) => r.id));
        const concurrencyLosers = stepIds.filter((id) => !returnedStepIds.has(id));

        return { reviewed, concurrencyLosers };
    };
}
