
import { Prisma } from "prisma/postgres/generated/prisma/client/postgres/client";
import { toDateFromString, sevenDaysFrom, toStringFromDate, toHHmmFromDate } from "src/common/utils/date-utils";
import { Timesheet } from "src/time-and-attendance/timesheets/timesheet.dto";

export const mapOneTimesheet = async (timesheet: Prisma.TimesheetsGetPayload<{
    include: {
        employee: { include: { user } },
        shift: { include: { bank_code }, orderBy: { start_time: 'asc' } },
        expense: { include: { bank_code, attachment_record } },
    }
}>): Promise<Timesheet> => {
    //converts string to UTC date format
    const start = toDateFromString(timesheet.start_date);
    const day_dates = sevenDaysFrom(start);

    //map of shifts by days
    const shifts_by_date = new Map<string, Prisma.ShiftsGetPayload<{ include: { bank_code }, orderBy: { start_time: 'asc' } }>[]>();
    for (const shift of timesheet.shift) {
        const date_string = toStringFromDate(shift.date);
        const arr = shifts_by_date.get(date_string) ?? [];
        arr.push(shift);
        shifts_by_date.set(date_string, arr);
    }
    //map of expenses by days
    const expenses_by_date = new Map<string, Prisma.ExpensesGetPayload<{ include: { bank_code: {}, attachment_record } }>[]>();
    for (const expense of timesheet.expense) {
        const date_string = toStringFromDate(expense.date);
        const arr = expenses_by_date.get(date_string) ?? [];
        arr.push(expense);
        expenses_by_date.set(date_string, arr);
    }
    //weekly totals
    const weekly_hours: TotalHours = emptyHours();
    const weekly_expenses: TotalExpenses = emptyExpenses();

    //map of days
    const days = day_dates.map((date) => {
        const date_iso = toStringFromDate(date);
        const shifts_source = shifts_by_date.get(date_iso) ?? [];
        const expenses_source = expenses_by_date.get(date_iso) ?? [];

        //inner map of shifts
        const shifts = shifts_source.map((shift) => ({
            timesheet_id: shift.timesheet_id,
            date: toStringFromDate(shift.date),
            start_time: toHHmmFromDate(shift.start_time),
            end_time: toHHmmFromDate(shift.end_time),
            type: shift.bank_code?.type ?? '',
            is_remote: shift.is_remote ?? false,
            is_approved: shift.is_approved ?? false,
            id: shift.id ?? null,
            comment: shift.comment ?? null,
        }));

        //inner map of expenses
        const expenses = expenses_source.map((expense) => ({
            date: toStringFromDate(expense.date),
            amount: expense.amount != null ? Number(expense.amount) : undefined,
            mileage: expense.mileage != null ? Number(expense.mileage) : undefined,
            id: expense.id ?? null,
            timesheet_id: expense.timesheet_id,
            attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined,
            is_approved: expense.is_approved ?? false,
            comment: expense.comment ?? '',
            supervisor_comment: expense.supervisor_comment,
            type: expense.bank_code.type,
        }));

        //daily totals
        const daily_hours = emptyHours();
        const daily_expenses = emptyExpenses();

        //totals by shift types
        for (const shift of shifts_source) {
            const hours = diffOfHours(shift.start_time, shift.end_time);
            const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
            const worked_weekly_hours = weekly_hours.regular + weekly_hours.emergency + weekly_hours.banking + weekly_hours.evening + weekly_hours.overtime + weekly_hours.holiday;
            
            if ((worked_weekly_hours + hours <= 40) && (subgroup === 'regular' || subgroup === 'evening')) {
                daily_hours['overtime'] += Math.max(daily_hours[subgroup] + hours - 8, 0);
                weekly_hours['overtime'] += Math.max(daily_hours[subgroup] + hours - 8, 0);
                
                weekly_hours[subgroup] += Math.min(hours, 8 - daily_hours[subgroup]);
                daily_hours[subgroup] += Math.min(hours, 8 - daily_hours[subgroup]);
            } else if (subgroup === 'regular' || subgroup === 'evening') {
                daily_hours[subgroup] += Math.max((40 - worked_weekly_hours), 0);
                daily_hours['overtime'] += Math.min((worked_weekly_hours + hours) - 40, hours);

                weekly_hours[subgroup] += Math.max((40 - worked_weekly_hours), 0);
                weekly_hours['overtime'] += Math.min((worked_weekly_hours + hours) - 40, hours);
            } else {
                daily_hours[subgroup] += hours;
                weekly_hours[subgroup] += hours;
            }
        }



        //totals by expense types
        for (const expense of expenses_source) {
            const subgroup = expenseSubgroupFromBankCode(expense.bank_code);
            if (subgroup === 'mileage') {
                const mileage = num(expense.mileage);
                daily_expenses.mileage += mileage;
                weekly_expenses.mileage += mileage;
            } else if (subgroup === 'per_diem') {
                const amount = num(expense.amount);
                daily_expenses.per_diem += amount;
                weekly_expenses.per_diem += amount;
            } else if (subgroup === 'on_call') {
                const amount = num(expense.amount);
                daily_expenses.on_call += amount;
                weekly_expenses.on_call += amount;
            } else {
                const amount = num(expense.amount);
                daily_expenses.expenses += amount;
                weekly_expenses.expenses += amount;
            }
        }
        return {
            date: date_iso,
            shifts,
            expenses,
            daily_hours,
            daily_expenses,
        };
    });

    return {
        timesheet_id: timesheet.id,
        is_approved: timesheet.is_approved ?? false,
        days,
        weekly_hours,
        weekly_expenses,
    };
}


//filled array with default values
const emptyHours = (): TotalHours => { return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, banking: 0, withdraw_banked: 0 } };
const emptyExpenses = (): TotalExpenses => { return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 } };

//calculate the differences of hours
const diffOfHours = (a: Date, b: Date): number => {
    const ms = new Date(b).getTime() - new Date(a).getTime();
    return Math.max(0, Math.round((ms / 36e5) * 1000) / 1000);
}

//validate numeric values
const num = (value: any): number => { return value ? Number(value) : 0 };

// shift's subgroup types 
const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => {
    const type = bank_code.type;
    if (type.includes('EVENING')) return 'evening';
    if (type.includes('EMERGENCY')) return 'emergency';
    if (type.includes('OVERTIME')) return 'overtime';
    if (type.includes('VACATION')) return 'vacation';
    if (type.includes('HOLIDAY')) return 'holiday';
    if (type.includes('SICK')) return 'sick';
    if (type.includes('BANKING')) return 'banking';
    if (type.includes('WITHDRAW_BANKED')) return 'withdraw_banked'
    return 'regular'
}

// expense's subgroup types
const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => {
    const type = bank_code.type;
    if (type.includes('MILEAGE')) return 'mileage';
    if (type.includes('PER_DIEM')) return 'per_diem';
    if (type.includes('ON_CALL')) return 'on_call';
    return 'expenses';
}

export type TotalHours = {
    regular: number;
    evening: number;
    emergency: number;
    overtime: number;
    vacation: number;
    holiday: number;
    sick: number;
    banking: number;
    withdraw_banked: number;
};

export type TotalExpenses = {
    expenses: number;
    per_diem: number;
    on_call: number;
    mileage: number;
};