import { toDateFromString, toStringFromHHmm, toStringFromDate, toDateFromHHmm, overlaps, computeHours } from "src/common/utils/date-utils";
import { Injectable } from "@nestjs/common";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";

import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper";
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
import { Result } from "src/common/errors/result-error.factory";

import { shift_select } from "src/time-and-attendance/utils/selects.utils";
import { Normalized } from "src/time-and-attendance/utils/type.utils";
import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service";
import { paid_time_off_types } from "src/time-and-attendance/paid-time-off/paid-time-off.dto";
import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service";

@Injectable()
export class ShiftsUpdateService {
    constructor(
        private readonly prisma: PrismaPostgresService,
        private readonly typeResolver: BankCodesResolver,
        private readonly timesheetResolver: EmployeeTimesheetResolver,
        private readonly emailResolver: EmailToIdResolver,
        private readonly paidTimeOffService: PaidTimeOFfBankHoursService,
        private readonly payPeriodEventService: PayPeriodEventService,
    ) { }

    async updateOneOrManyShifts(shifts: ShiftDto[], email: string, is_from_timesheet: boolean = true): Promise<Result<boolean, string>> {
        try {
            //verify if array is empty or not
            if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' };

            //check for overlap inside dto objects
            const overlap_check = await this.overlapChecker(shifts);
            if (!overlap_check.success) return overlap_check;

            //calls the update functions and await the return of successfull result or not
            const results = await Promise.allSettled(shifts.map(shift => this.updateShift(shift, email)));

            //return arrays of updated shifts or errors
            const updated_shifts: ShiftDto[] = [];
            const errors: string[] = [];

            //filters results into updated_shifts or errors arrays depending on the return from "allSettled" Promise
            for (const result of results) {
                if (result.status === 'fulfilled') {
                    if (result.value.success) {
                        updated_shifts.push(result.value.data);
                    } else {
                        errors.push(result.value.error);
                    }
                } else {
                    errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason));
                }
            }
            //verify if shifts were updated and returns an array of errors if needed
            if (updated_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift updated' };

            // push to event service to notify timesheet-approval subscribers of change
            if (is_from_timesheet) {
                this.payPeriodEventService.emit({
                    employee_email: email,
                    event_type: 'shift',
                    action: 'update'
                })
            }

            // returns array of updated shifts
            return { success: true, data: true }
        } catch (error) {
            return { success: false, error }
        }
    }
    //_________________________________________________________________
    //                              UPDATE
    //_________________________________________________________________
    async updateShift(dto: ShiftDto, email: string): Promise<Result<ShiftDto, string>> {
        try {
            const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, toDateFromString(dto.date));
            if (!timesheet.success) return { success: false, error: timesheet.error };
            const employee = await this.emailResolver.findIdByEmail(email);
            if (!employee.success) return { success: false, error: employee.error };
            //finds original shift
            const original = await this.prisma.shifts.findFirst({
                where: { id: dto.id, timesheet_id: timesheet.data.id },
                select: shift_select,
            });
            if (!original) return { success: false, error: `SHIFT_NOT_FOUND` };

            //transform string format to date and HHmm
            const normed_shift = await this.normalizeShiftDto(dto);
            if (!normed_shift.success) return { success: false, error: normed_shift.error }
            if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT` };

            //finds bank_code_id using the type
            const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type);
            if (!bank_code.success) return { success: false, error: bank_code.error };

            const original_type = original.bank_code.type;
            const new_type = dto.type;
            const type_changed = original_type !== new_type;

            //call to ajust paid_time_off hour banks
            if (paid_time_off_types.includes(original_type) || paid_time_off_types.includes(new_type)) {
                if (type_changed) {
                    let original_hours = computeHours(original.start_time, original.end_time);
                    if (original_type === 'BANKING') {
                        original_hours = original_hours * 1.5;
                    }
                    if (paid_time_off_types.includes(original_type)) {
                        const restoration = await this.paidTimeOffService.restorePaidTimeOffHours(employee.data, original_type, original_hours);
                        if (!restoration.success) return { success: false, error: restoration.error };
                    }
                    if (paid_time_off_types.includes(new_type)) {
                        let new_hours = computeHours(normed_shift.data.start_time, normed_shift.data.end_time);
                        if (new_type === 'BANKING') {
                            new_hours = new_hours * 1.5;
                        }
                        const validation = await this.paidTimeOffService.validateAndDeductPaidTimeOff(employee.data, new_type, new_hours);
                        if (!validation.success) return { success: false, error: validation.error };
                    }
                } else {
                    const result = await this.paidTimeOffService.updatePaidTimeOffBankHoursWhenShiftUpdate(
                        normed_shift.data.start_time,
                        normed_shift.data.end_time,
                        dto.type,
                        employee.data,
                        original.start_time,
                        original.end_time
                    );
                    if (!result.success) return { success: false, error: result.error };
                }

            }

            //updates sent to DB
            const updated = await this.prisma.shifts.update({
                where: { id: original.id },
                data: {
                    date: normed_shift.data.date,
                    start_time: normed_shift.data.start_time,
                    end_time: normed_shift.data.end_time,
                    bank_code_id: bank_code.data,
                    comment: dto.comment,
                    is_approved: dto.is_approved,
                    is_remote: dto.is_remote,
                },
                select: shift_select,
            });
            if (!updated) return { success: false, error: 'INVALID_SHIFT' };

            // builds an object to return for display in the frontend
            const shift: ShiftDto = {
                id: updated.id,
                timesheet_id: updated.timesheet_id,
                type: dto.type,
                date: toStringFromDate(updated.date),
                start_time: toStringFromHHmm(updated.start_time),
                end_time: toStringFromHHmm(updated.end_time),
                is_approved: updated.is_approved,
                is_remote: updated.is_remote,
                comment: updated.comment ?? '',
            }
            return { success: true, data: shift };

        } catch (error) {
            return { success: false, error: `INVALID_SHIFT` };
        }
    }

    //_________________________________________________________________
    //                              helpers
    //_________________________________________________________________
    private normalizeShiftDto = async (dto: ShiftDto): Promise<Result<Normalized, string>> => {
        const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
        if (!bank_code_id.success) return { success: false, error: 'INVALID_SHIFT' }
        return {
            success: true,
            data: {
                date: toDateFromString(dto.date),
                start_time: toDateFromHHmm(dto.start_time),
                end_time: toDateFromHHmm(dto.end_time),
                bank_code_id: bank_code_id.data
            }
        };
    }

    private overlapChecker = async (shifts: ShiftDto[]): Promise<Result<void, string>> => {
        for (let i = 0; i < shifts.length; i++) {
            for (let j = i + 1; j < shifts.length; j++) {
                const shift_a = shifts[i];
                const shift_b = shifts[j];

                if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue;
                const has_overlap = overlaps(
                    { start: toDateFromHHmm(shift_a.start_time), end: toDateFromHHmm(shift_a.end_time) },
                    { start: toDateFromHHmm(shift_b.start_time), end: toDateFromHHmm(shift_b.end_time) },
                );
                if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` };
            }
        }
        return { success: true, data: undefined }
    }
}