import { BadRequestError, BaseAdapter, ErrorCodes, EventEmitterPort, Events } from '@humand-packages/common';
import { Service } from 'typedi';
import { PortsDI } from '../../../../../config/portsDI';
import { UsersService } from '../../../../users/business/services/usersService';
import { UserDAO } from '../../../../users/infrastructure/dataAccessObjects/userDAO';
import ShiftAssignedDayRepositoryPort from '../../../infrastructure/ports/shiftManagement/shiftAssignedDayRepositoryPort';
import ShiftsRepositoryPort from '../../../infrastructure/ports/shiftManagement/shiftRepositoryPort';
import { ShiftBulkProcessResult } from '../../../presentation/dtos/shiftBulkProcessResult';
import { mapShiftAssignedDayToResponse } from '../../../presentation/mappers/mapShiftAssignedDayToResponse';
import { CategorizedHoursActionType } from '../../constants/categorizedHoursActionTypes';
import { AssignedShiftsEmployeeRequestInterface } from '../../interfaces/shiftManagement/assignedShiftsEmployeeRequestInterface';
import { AssignShiftsBulkRequestInterface } from '../../interfaces/shiftManagement/assignShiftsBulkRequestInterface';
import { DayEntryRequestInterface } from '../../interfaces/shiftManagement/dayEntryRequestInterface';
import { Shift } from '../../models/shiftManagement/shift';
import { ShiftAssignedDay, ShiftAssignedDayForCreation } from '../../models/shiftManagement/shiftAssignedDay';
import { RawShiftAssignedShiftForCreation, ShiftAssignedShift, ShiftAssignedShiftForCreation } from '../../models/shiftManagement/shiftAssignedShift';
import {
    RawShiftAssignedTimeSlotForCreation,
    ShiftAssignedTimeSlot,
    ShiftAssignedTimeSlotForCreation,
} from '../../models/shiftManagement/shiftAssignedTimeSlot';
import { TimeSlot } from '../../models/timeTracking/scheduleData';
import ScheduleCategorizedHoursPort from '../../ports/scheduleCategorizedHoursPort';
import { ShiftAssignedDayStatus } from '../../types/shiftAssignedDayStatus';
import { ShiftAssignedShiftStatus } from '../../types/shiftManagement/assignedShiftStatus';
import { ShiftAssignedShiftType } from '../../types/shiftManagement/assignedShiftType';
import { SkippedShiftEntry } from '../../types/shiftManagement/skippedShiftEntry';
import { ShiftAssignedShiftSource } from '../../types/shiftManagement/source';

@Service()
export default class ShiftAssignedDayService extends BaseAdapter {
    constructor(
        @PortsDI(ShiftAssignedDayRepositoryPort) private readonly shiftAssignedDayRepository: ShiftAssignedDayRepositoryPort,
        @PortsDI(ShiftsRepositoryPort) private readonly shiftsRepository: ShiftsRepositoryPort,
        private readonly usersService: UsersService,
        private readonly eventEmitter: EventEmitterPort,
        @PortsDI(ScheduleCategorizedHoursPort) private readonly scheduleCategorizedHoursPort: ScheduleCategorizedHoursPort,
    ) {
        super();
    }

    public bulkProcessShiftsApi = async (
        instanceId: number,
        assignShiftsBulkRequest: AssignShiftsBulkRequestInterface,
    ): Promise<ShiftBulkProcessResult> => {
        const employeeIds = assignShiftsBulkRequest.employees.map((employee) => employee.employeeId);
        const users = await this.usersService.findAllByEmployeeInternalId(instanceId, employeeIds);

        const processed: ShiftAssignedDay[] = [];
        const skipped: SkippedShiftEntry[] = [];

        const userMap = new Map<string, UserDAO>();
        const userIdToEmployeeIdMap = new Map<number, string>();
        for (const u of users) {
            userMap.set(u.employeeInternalId, u);
            userIdToEmployeeIdMap.set(u.id, u.employeeInternalId);
        }

        const templateMap = await this.getTemplateMap(instanceId, assignShiftsBulkRequest);

        const shiftAssignedDaysForCreation: ShiftAssignedDayForCreation[] = [];
        const shiftAssignedShiftsForCreationMap: Map<string, Map<string, RawShiftAssignedShiftForCreation[]>> = new Map();
        const shiftAssignedTimeSlotsForCreationMap: Map<string, Map<string, RawShiftAssignedTimeSlotForCreation[]>> = new Map();
        for (const employee of assignShiftsBulkRequest.employees) {
            const foundUser = userMap.get(employee.employeeId);
            if (!foundUser) {
                // No user found, skip all days for this employee
                for (const dayEntry of employee.days) {
                    skipped.push(this.createSkippedEntry(employee.employeeId, dayEntry.date, ErrorCodes.USER_NOT_FOUND, 'User not found'));
                }
                continue;
            } else {
                for (const dayEntry of employee.days) {
                    if (dayEntry.shiftTemplateId != null) {
                        const template = templateMap.get(dayEntry.shiftTemplateId);
                        if (!template) {
                            skipped.push(
                                this.createSkippedEntry(
                                    employee.employeeId,
                                    dayEntry.date,
                                    ErrorCodes.SHIFT_MANAGEMENT_SHIFT_NOT_FOUND,
                                    `shift_template_id ${dayEntry.shiftTemplateId} not found for this tenant`,
                                ),
                            );
                            continue;
                        }
                        if (!dayEntry.isWorkday) {
                            skipped.push(
                                this.createSkippedEntry(
                                    employee.employeeId,
                                    dayEntry.date,
                                    ErrorCodes.SHIFT_MANAGEMENT_INVALID_DAY_CONFIGURATION,
                                    'shift_template_id cannot be used on a non-workday (isWorkday=false)',
                                ),
                            );
                            continue;
                        }
                        shiftAssignedDaysForCreation.push({
                            dateString: dayEntry.date,
                            instanceId,
                            status: ShiftAssignedDayStatus.PUBLISHED,
                            userId: foundUser.id,
                        });
                        // Linked to a repository template: source=APP, no time-slot snapshot — name/slots resolve from the template via shiftCode/shiftVersion.
                        this.setShiftAssignedShiftForCreationInMap(instanceId, shiftAssignedShiftsForCreationMap, employee, dayEntry, template);
                        continue;
                    }
                    try {
                        this.validateTimeSlotsConfiguration(dayEntry.timeSlots, dayEntry.isWorkday);
                    } catch (error) {
                        if (error instanceof BadRequestError) {
                            skipped.push(this.createSkippedEntry(employee.employeeId, dayEntry.date, error.code, error.message));
                            continue;
                        } else {
                            throw error;
                        }
                    }
                    shiftAssignedDaysForCreation.push({
                        dateString: dayEntry.date,
                        instanceId,
                        status: ShiftAssignedDayStatus.PUBLISHED,
                        userId: foundUser.id,
                    });

                    this.setShiftAssignedShiftForCreationInMap(instanceId, shiftAssignedShiftsForCreationMap, employee, dayEntry);
                    this.setShiftAssignedTimeSlotsForCreationInMap(shiftAssignedTimeSlotsForCreationMap, employee, dayEntry);
                }
            }
        }

        if (shiftAssignedDaysForCreation.length === 0) {
            return { processed: [], skipped };
        }

        const shiftAssignedDayMap = new Map<number, ShiftAssignedDay>();
        const shiftAssignedDayIds: number[] = [];
        await this.shiftAssignedDayRepository.getTransaction(async () => {
            const shiftAssignedDays = await this.shiftAssignedDayRepository.bulkCreateShiftAssignedDays(shiftAssignedDaysForCreation);
            const shiftAssignedShiftsForCreation: ShiftAssignedShiftForCreation[] = [];
            for (const shiftAssignedDay of shiftAssignedDays) {
                shiftAssignedDayMap.set(shiftAssignedDay.id, shiftAssignedDay);
                shiftAssignedDayIds.push(shiftAssignedDay.id);
                const employeeId = userIdToEmployeeIdMap.get(shiftAssignedDay.userId);
                if (!employeeId) continue;
                const rawShiftAssignedShiftsForCreation = shiftAssignedShiftsForCreationMap.get(employeeId)?.get(shiftAssignedDay.dateString);
                if (!rawShiftAssignedShiftsForCreation) continue;
                shiftAssignedShiftsForCreation.push(
                    ...rawShiftAssignedShiftsForCreation.map((rawShiftAssignedShiftForCreation) => ({
                        ...rawShiftAssignedShiftForCreation,
                        assignedDayId: shiftAssignedDay.id,
                    })),
                );
            }
            await this.shiftAssignedDayRepository.bulkDeleteShiftAssignedDayAssociations(instanceId, shiftAssignedDayIds);
            const shiftAssignedShifts = await this.shiftAssignedDayRepository.bulkCreateShiftAssignedShifts(shiftAssignedShiftsForCreation);
            const shiftAssignedShiftsMap = new Map<number, ShiftAssignedShift[]>();
            const shiftAssignedTimeSlotsForCreation: ShiftAssignedTimeSlotForCreation[] = [];
            for (const shiftAssignedShift of shiftAssignedShifts) {
                if (!shiftAssignedShiftsMap.has(shiftAssignedShift.assignedDayId)) {
                    shiftAssignedShiftsMap.set(shiftAssignedShift.assignedDayId, []);
                }
                shiftAssignedShiftsMap.get(shiftAssignedShift.assignedDayId)?.push(shiftAssignedShift);
                const shiftAssignedDay = shiftAssignedDayMap.get(shiftAssignedShift.assignedDayId);
                if (!shiftAssignedDay) continue;
                const employeeId = userIdToEmployeeIdMap.get(shiftAssignedDay.userId);
                if (!employeeId) continue;
                const rawShiftAssignedTimeSlotsForCreation = shiftAssignedTimeSlotsForCreationMap.get(employeeId)?.get(shiftAssignedDay.dateString);
                if (!rawShiftAssignedTimeSlotsForCreation) continue;
                shiftAssignedTimeSlotsForCreation.push(
                    ...rawShiftAssignedTimeSlotsForCreation.map((rawShiftAssignedTimeSlotForCreation) => ({
                        ...rawShiftAssignedTimeSlotForCreation,
                        assignedShiftId: shiftAssignedShift.id,
                    })),
                );
            }
            const shiftAssignedTimeSlots = await this.shiftAssignedDayRepository.bulkCreateShiftAssignedTimeSlots(shiftAssignedTimeSlotsForCreation);
            const shiftAssignedTimeSlotsMap = new Map<number, ShiftAssignedTimeSlot[]>();
            for (const shiftAssignedTimeSlot of shiftAssignedTimeSlots) {
                if (!shiftAssignedTimeSlotsMap.has(shiftAssignedTimeSlot.assignedShiftId)) {
                    shiftAssignedTimeSlotsMap.set(shiftAssignedTimeSlot.assignedShiftId, []);
                }
                shiftAssignedTimeSlotsMap.get(shiftAssignedTimeSlot.assignedShiftId)?.push(shiftAssignedTimeSlot);
            }
            for (const shiftAssignedShift of shiftAssignedShifts) {
                if (!shiftAssignedTimeSlotsMap.has(shiftAssignedShift.id)) continue;
                shiftAssignedShift.timeSlots = shiftAssignedTimeSlotsMap.get(shiftAssignedShift.id)!;
            }

            for (const shiftAssignedDay of shiftAssignedDays) {
                if (!shiftAssignedShiftsMap.has(shiftAssignedDay.id)) continue;
                shiftAssignedDay.shiftAssociations = shiftAssignedShiftsMap.get(shiftAssignedDay.id)!;
            }
            if (shiftAssignedDays.length > 0) {
                const dates = shiftAssignedDays.map((d) => d.dateString);
                const userIds = [...new Set(shiftAssignedDays.map((d) => d.userId))];
                const minDate = dates.reduce((a, b) => (a < b ? a : b));
                const maxDate = dates.reduce((a, b) => (a > b ? a : b));
                await this.scheduleCategorizedHoursPort.schedule({
                    type: CategorizedHoursActionType.SCHEDULE_CHANGE,
                    payload: { instanceId, userIds, startDate: minDate, endDate: maxDate },
                });
            }
            processed.push(...shiftAssignedDays);
        });

        if (processed.length > 0) {
            this.emitShiftAssignedDayCreatedEvent(processed, instanceId);
        }

        return {
            processed: processed.map((shift) => mapShiftAssignedDayToResponse(shift, userIdToEmployeeIdMap.get(shift.userId) || '')),
            skipped,
        };
    };

    private async getTemplateMap(instanceId: number, request: AssignShiftsBulkRequestInterface): Promise<Map<number, Shift>> {
        const templateIds = [
            ...new Set(
                request.employees
                    .flatMap((employee) => employee.days)
                    .map((dayEntry) => dayEntry.shiftTemplateId)
                    .filter((id): id is number => id != null),
            ),
        ];
        if (templateIds.length === 0) return new Map();
        const templates = await this.shiftsRepository.getTemplatesByIds(instanceId, templateIds);
        return new Map(templates.map((template) => [template.id, template]));
    }

    private setShiftAssignedShiftForCreationInMap(
        instanceId: number,
        shiftAssignedShiftsForCreationMap: Map<string, Map<string, RawShiftAssignedShiftForCreation[]>>,
        employee: AssignedShiftsEmployeeRequestInterface,
        dayEntry: DayEntryRequestInterface,
        template?: Shift,
    ) {
        if (!shiftAssignedShiftsForCreationMap.has(employee.employeeId)) {
            shiftAssignedShiftsForCreationMap.set(employee.employeeId, new Map());
        }
        if (!shiftAssignedShiftsForCreationMap.get(employee.employeeId)?.has(dayEntry.date)) {
            shiftAssignedShiftsForCreationMap.get(employee.employeeId)?.set(dayEntry.date, []);
        }
        const rawShiftForCreation: RawShiftAssignedShiftForCreation = template
            ? {
                  instanceId,
                  shiftCode: template.code,
                  shiftVersion: template.version,
                  source: ShiftAssignedShiftSource.APP,
                  status: ShiftAssignedShiftStatus.PUBLISHED,
                  timeSlots: [],
                  type: ShiftAssignedShiftType.WORK,
              }
            : {
                  instanceId,
                  source: ShiftAssignedShiftSource.INTEGRATION,
                  status: ShiftAssignedShiftStatus.PUBLISHED,
                  timeSlots: [],
                  type: dayEntry.isWorkday ? ShiftAssignedShiftType.WORK : ShiftAssignedShiftType.REST,
              };
        shiftAssignedShiftsForCreationMap.get(employee.employeeId)?.get(dayEntry.date)?.push(rawShiftForCreation);
    }

    private setShiftAssignedTimeSlotsForCreationInMap(
        shiftAssignedTimeSlotsForCreationMap: Map<string, Map<string, RawShiftAssignedTimeSlotForCreation[]>>,
        employee: AssignedShiftsEmployeeRequestInterface,
        dayEntry: DayEntryRequestInterface,
    ) {
        if (!shiftAssignedTimeSlotsForCreationMap.has(employee.employeeId)) {
            shiftAssignedTimeSlotsForCreationMap.set(employee.employeeId, new Map());
        }
        if (!shiftAssignedTimeSlotsForCreationMap.get(employee.employeeId)?.has(dayEntry.date)) {
            shiftAssignedTimeSlotsForCreationMap.get(employee.employeeId)?.set(dayEntry.date, []);
        }

        for (const timeSlotTuple of dayEntry.timeSlots) {
            const [startTime, endTime] = timeSlotTuple;
            const timeSlotForCreation: RawShiftAssignedTimeSlotForCreation = {
                endTime,
                startTime,
            };
            shiftAssignedTimeSlotsForCreationMap.get(employee.employeeId)?.get(dayEntry.date)?.push(timeSlotForCreation);
        }
    }

    public emitShiftAssignedDayCreatedEvent = (shiftAssignedDays: ShiftAssignedDay[], instanceId: number, deleteDaySummaries = true) => {
        const shiftsByDate = new Map<string, number[]>();
        for (const shift of shiftAssignedDays) {
            if (!shiftsByDate.has(shift.dateString)) {
                shiftsByDate.set(shift.dateString, []);
            }
            shiftsByDate.get(shift.dateString)?.push(shift.userId);
        }
        for (const [date, userIds] of shiftsByDate.entries()) {
            this.eventEmitter.emitEvent(Events.SHIFT_ASSIGNED_DAY_CREATED, {
                deleteDaySummaries,
                endDate: date,
                instanceId,
                startDate: date,
                userIds: userIds,
            });
        }
    };

    private validateTimeSlotsConfiguration(timeSlots: [string, string][], isWorkday: boolean) {
        if (!isWorkday) {
            if (timeSlots && timeSlots.length > 0) {
                throw new BadRequestError('Non-workday must not have time slots', ErrorCodes.SHIFT_MANAGEMENT_INVALID_DAY_CONFIGURATION);
            }
        }

        const validatedSlots: { start: string; end: string }[] = [];

        for (const [startTimeRaw, endTimeRaw] of timeSlots) {
            const startTime = startTimeRaw.trim();
            const endTime = endTimeRaw.trim();

            if (startTime === endTime) {
                throw new BadRequestError(
                    `Start time (${startTime}) must not be equal to end time (${endTime})`,
                    ErrorCodes.SHIFT_MANAGEMENT_INVALID_TIME_SLOTS,
                );
            }

            for (const slot of validatedSlots) {
                if (this.hasTimeOverlap(startTime, endTime, slot.start, slot.end)) {
                    throw new BadRequestError(
                        `Overlapping time slots found: [${startTime}, ${endTime}] overlaps with [${slot.start}, ${slot.end}]`,
                        ErrorCodes.SHIFT_MANAGEMENT_INVALID_TIME_SLOTS,
                    );
                }
            }

            validatedSlots.push({ end: endTime, start: startTime });
        }
    }

    private hasTimeOverlap(start1: string, end1: string, start2: string, end2: string): boolean {
        return (start1 >= start2 && start1 < end2) || (end1 > start2 && end1 <= end2) || (start1 <= start2 && end1 >= end2);
    }

    private createSkippedEntry(employeeId: string, date: string, code: ErrorCodes, message: string): SkippedShiftEntry {
        return {
            date,
            employeeId,
            reason: {
                code: code.toString(),
                message,
            },
        };
    }

    public getShiftAssignedDaysByUser = async (userId: number, instanceId: number, fromDate: string, toDate: string): Promise<ShiftAssignedDay[]> => {
        return await this.shiftAssignedDayRepository.getShiftAssignedDaysByUser(userId, instanceId, fromDate, toDate);
    };

    public getShiftAssignedDayById = async (id: number): Promise<ShiftAssignedDay | null> => {
        return await this.shiftAssignedDayRepository.getShiftAssignedDayById(id);
    };

    public getTimeSlotFromShiftAssignedDay = (shiftAssignedDay?: ShiftAssignedDay): TimeSlot[] => {
        const timeSlots: TimeSlot[] = [];
        shiftAssignedDay?.shiftAssociations?.forEach((shift) => {
            switch (shift.source) {
                case ShiftAssignedShiftSource.INTEGRATION:
                    timeSlots.push(...(shift.timeSlots || []));
                    break;
                case ShiftAssignedShiftSource.APP:
                    timeSlots.push(...(shift.shift?.timeSlots || []));
                    break;
                default:
                    break;
            }
        });
        return timeSlots;
    };
}
