import { Normalized } from "src/time-and-attendance/utils/type.utils";
import { Injectable } from "@nestjs/common";
import { timesheet_select } from "src/time-and-attendance/utils/selects.utils";
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { Result } from "src/common/errors/result-error.factory";
import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toDateFromHHmm, computeHours } from "src/common/utils/date-utils";
import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto";
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service";
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.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 ShiftsCreateService {
    constructor(
        private readonly prisma: PrismaPostgresService,
        private readonly emailResolver: EmailToIdResolver,
        private readonly typeResolver: BankCodesResolver,
        private readonly vacationService: VacationService,
        private readonly bankingService: BankedHoursService,
        private readonly sickService: SickLeaveService,
        private readonly payPeriodEventService: PayPeriodEventService,
    ) { }

    //_________________________________________________________________
    //           CREATE WRAPPER FUNCTION FOR ONE OR MANY INPUT
    //_________________________________________________________________
    async createOneOrManyShifts(email: string, shifts: ShiftDto[], 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' };

            //verify if email is valid or not
            const employee_id = await this.emailResolver.findIdByEmail(email);
            if (!employee_id.success) return { success: false, error: employee_id.error };

            //calls the create functions and await the return of successfull result or not
            const results = await Promise.allSettled(shifts.map(shift => this.createShift(employee_id.data, shift)));

            //return arrays of created shifts or errors
            const created_shifts: ShiftDto[] = [];
            const errors: string[] = [];

            //filters results into created_shifts or errors arrays depending on the return from "allSettled" Promise
            for (const result of results) {
                if (result.status === 'fulfilled') {
                    if (result.value.success) {
                        created_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 created and returns an array of errors if needed
            if (created_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift created' };

            // 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: 'create'
                })
            }

            // returns array of created shifts
            return { success: true, data: true }
        } catch (error) {
            return { success: false, error }
        }
    }
    //_________________________________________________________________
    //                           CREATE
    //_________________________________________________________________
    async createShift(employee_id: number, dto: ShiftDto): Promise<Result<ShiftDto, string>> {
        try {
            //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_TIME` };
            //fetch the right timesheet
            const timesheet = await this.prisma.timesheets.findUnique({
                where: { id: dto.timesheet_id, employee_id },
                select: timesheet_select,
            });
            if (!timesheet) return { success: false, error: `INVALID_TIMESHEET` };
            //finds bank_code_id using the type
            const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type);
            if (!bank_code_id.success) return { success: false, error: bank_code_id.error };

            //fetchs existing shifts from DB to check for overlaps
            const existing_shifts = await this.prisma.shifts.findMany({
                where: { timesheet_id: timesheet.id, date: normed_shift.data.date },
                select: { id: true, date: true, start_time: true, end_time: true },
            });
            for (const existing of existing_shifts) {
                const existing_start = await toDateFromString(existing.start_time);
                const existing_end = await toDateFromString(existing.end_time);
                const existing_date = await toDateFromString(existing.date);

                const has_overlap = overlaps(
                    { start: normed_shift.data.start_time, end: normed_shift.data.end_time, date: normed_shift.data.date },
                    { start: existing_start, end: existing_end, date: existing_date },
                );
                if (has_overlap) {
                    return { success: false, error: `SHIFT_OVERLAP` };
                }
            }

            let adjusted_end_time = normed_shift.data.end_time;


            if (paid_time_off_types.includes(dto.type)) {
                let result: Result<number, string>;

                let amount_hours = computeHours(normed_shift.data.start_time, normed_shift.data.end_time);
                const banking_types: string[] = ['BANKING', 'WITHDRAW_BANKED'];

                if (banking_types.includes(dto.type)) {
                    if (dto.type === 'BANKING') {
                        amount_hours = amount_hours * 1.5;
                    }
                    result = await this.bankingService.manageBankingHours(employee_id, amount_hours, dto.type);
                } else {
                    switch (dto.type) {
                        case 'VACATION': {
                            result = await this.vacationService.manageVacationHoursBank(employee_id, amount_hours);
                            break;
                        }
                        case 'SICK': {
                            result = await this.sickService.takeSickLeaveHours(employee_id, amount_hours);
                            break;
                        }
                        default:
                            result = { success: false, error: 'INVALID_PAID_TIME_OFF_TYPE' };
                            break;
                    }
                }

                if (!result.success) return { success: false, error: result.error };
                const valid_hours = result.data / 1.5;
                adjusted_end_time = new Date(normed_shift.data.start_time);
                adjusted_end_time.setHours(adjusted_end_time.getHours() + valid_hours);
            }

            //sends data for creation of a shift in db
            const created_shift = await this.prisma.shifts.create({
                data: {
                    timesheet_id: timesheet.id,
                    bank_code_id: bank_code_id.data,
                    date: normed_shift.data.date,
                    start_time: normed_shift.data.start_time,
                    end_time: adjusted_end_time,
                    is_approved: dto.is_approved,
                    is_remote: dto.is_remote,
                    comment: dto.comment ?? '',
                },
            });

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

    //_________________________________________________________________
    //                         LOCAL HELPERS
    //_________________________________________________________________
    //converts all string hours and date to Date and HHmm formats
    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' }

        //TODO: validate date and time to ensure "banana" is not accepted using an if statement and a REGEX
        const date = toDateFromString(dto.date);
        const start_time = toDateFromHHmm(dto.start_time);
        const end_time = toDateFromHHmm(dto.end_time);

        return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
    }
}
