import { ENV_OPTIONAL_NUMBER_KEYS_MAPPING, 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 { LimitOffsetPagination } from '../../../../../../types/listOptions';
import {
    ShiftRotation,
    ShiftRotationDay,
    ShiftRotationDayShift,
    ShiftRotationForCreation,
} from '../../../../business/models/shiftManagement/shiftRotation';
import { ShiftRotationDAO } from '../../../dataAccessObjects/shiftManagement/shiftRotation';
import { ShiftRotationDayDAO } from '../../../dataAccessObjects/shiftManagement/shiftRotationDay';
import { ShiftRotationDayShiftDAO } from '../../../dataAccessObjects/shiftManagement/shiftRotationDayShift';
import {
    buildShiftRotation,
    shiftRotationDAOToShiftRotation,
    shiftRotationDayDAOToShiftRotationDay,
    shiftRotationDayShiftDAOToShiftRotationDayShift,
} from '../../../mappers/shiftManagement/shiftRotationMapper';
import ShiftRotationRepositoryPort, { ShiftRotationsListFilters } from '../../../ports/shiftManagement/shiftRotationRepositoryPort';

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

    public create = async (instanceId: number, forCreation: ShiftRotationForCreation): Promise<ShiftRotation> => {
        const rotationId = uuidv7();
        const rotationDao = await ShiftRotationDAO.create({
            description: forCreation.description,
            id: rotationId,
            instanceId,
            name: forCreation.name,
        });
        const days = await this.insertDays(instanceId, rotationId, forCreation);
        return shiftRotationDAOToShiftRotation(rotationDao, days);
    };

    public getById = async (instanceId: number, id: string): Promise<ShiftRotation | null> => {
        const dao = await ShiftRotationDAO.findOne({
            include: [
                {
                    model: ShiftRotationDayDAO,
                    required: false,
                    include: [{ model: ShiftRotationDayShiftDAO, required: false }],
                },
            ],
            order: [[{ model: ShiftRotationDayDAO, as: 'days' }, 'dayPosition', 'ASC']],
            where: { id, instanceId },
        });
        return dao ? shiftRotationDAOToShiftRotation(dao) : null;
    };

    public list = async (
        instanceId: number,
        filters: ShiftRotationsListFilters,
        pagination: LimitOffsetPagination,
    ): Promise<{ count: number; rows: ShiftRotation[] }> => {
        const where: { instanceId: number; name?: { [Op.iLike]: string } } = { instanceId };
        if (filters.search && filters.search.trim().length > 0) {
            where.name = { [Op.iLike]: `%${filters.search.trim()}%` };
        }

        const { count, rows } = await ShiftRotationDAO.findAndCountAll({
            distinct: true,
            include: [
                {
                    model: ShiftRotationDayDAO,
                    required: false,
                    include: [{ model: ShiftRotationDayShiftDAO, required: false }],
                },
            ],
            limit: pagination.limit,
            offset: pagination.offset,
            order: [['name', 'ASC']],
            where,
        });

        return { count, rows: rows.map((dao) => shiftRotationDAOToShiftRotation(dao)) };
    };

    public replaceDays = async (instanceId: number, existing: ShiftRotation, forCreation: ShiftRotationForCreation): Promise<ShiftRotation> => {
        await ShiftRotationDAO.update({ description: forCreation.description, name: forCreation.name }, { where: { id: existing.id, instanceId } });

        const existingDayIds = existing.days.map((d) => d.id);
        if (existingDayIds.length > 0) {
            await ShiftRotationDayShiftDAO.destroy({ where: { instanceId, shiftRotationDayId: existingDayIds } });
            await ShiftRotationDayDAO.destroy({ where: { id: existingDayIds, instanceId } });
        }

        const days = await this.insertDays(instanceId, existing.id, forCreation);

        return buildShiftRotation(
            {
                createdAt: existing.createdAt,
                description: forCreation.description,
                id: existing.id,
                instanceId: existing.instanceId,
                name: forCreation.name,
                updatedAt: new Date(),
            },
            days,
        );
    };

    public softDelete = async (instanceId: number, id: string): Promise<boolean> => {
        const rotation = await ShiftRotationDAO.findOne({ where: { id, instanceId } });
        if (!rotation) return false;

        const dayIds = (
            await ShiftRotationDayDAO.findAll({
                attributes: ['id'],
                where: { instanceId, shiftRotationId: id },
            })
        ).map((d) => d.id);

        if (dayIds.length > 0) {
            await ShiftRotationDayShiftDAO.destroy({
                where: { instanceId, shiftRotationDayId: dayIds },
            });
            await ShiftRotationDayDAO.destroy({ where: { id: dayIds, instanceId } });
        }

        await rotation.destroy();
        return true;
    };

    private insertDays = async (instanceId: number, rotationId: string, forCreation: ShiftRotationForCreation): Promise<ShiftRotationDay[]> => {
        const dayRows = forCreation.days.map((day, i) => ({
            dayPosition: i + 1,
            id: uuidv7(),
            instanceId,
            shiftRotationId: rotationId,
            type: day.type,
        }));
        const dayDaos = await ShiftRotationDayDAO.bulkCreate(dayRows);

        const shiftRows = forCreation.days.flatMap((day, i) =>
            day.shifts.map((shift) => ({
                id: uuidv7(),
                instanceId,
                shiftCode: shift.shiftCode,
                shiftRotationDayId: dayRows[i].id,
            })),
        );
        const shiftDaos = shiftRows.length > 0 ? await ShiftRotationDayShiftDAO.bulkCreate(shiftRows) : [];

        const shiftsByDayId = new Map<string, ShiftRotationDayShift[]>();
        for (const sd of shiftDaos) {
            const arr = shiftsByDayId.get(sd.shiftRotationDayId) ?? [];
            arr.push(shiftRotationDayShiftDAOToShiftRotationDayShift(sd));
            shiftsByDayId.set(sd.shiftRotationDayId, arr);
        }

        return dayDaos
            .map((d) => {
                const shifts = (shiftsByDayId.get(d.id) ?? []).sort((a, b) => a.id.localeCompare(b.id));
                return shiftRotationDayDAOToShiftRotationDay(d, shifts);
            })
            .sort((a, b) => a.dayPosition - b.dayPosition);
    };
}
