import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Filters, CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto";
import { computeHours } from "src/common/utils/date-utils";
import { applyHolidayRequalifications, applyOvertimeRequalifications, computeWeekNumber, consolidateRowHoursAndAmountByType, formatDate, resolveCompanyCodes } from "src/time-and-attendance/exports/csv-exports.utils";
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
import { select_csv_expense_lines, select_csv_shift_lines } from "src/time-and-attendance/utils/selects.utils";
import { BillableShiftType } from "src/time-and-attendance/shifts/shift.dto";


@Injectable()
export class CsvExportService {
    constructor(
        private readonly prisma: PrismaPostgresService,
        private readonly holiday_service: HolidayService,
    ) { }

    /**
     * Gets all approved shifts and expenses within the year and period number provided, according 
     * to company and types specified in the filters. Any holiday shifts will be adjusted based on 
     * prior 4 weeks of work, where applicable. Vacation and Sick shifts will be split into separate 
     * lines if they are more than one day apart.
     * 
     * ex: A pay period goes from January 10 to 24. An employee has vacation shifts from 
     * January 11 to
     * 13 and one shift on Jan 23, then two separate lines will be generated; one from Jan 11 to 13 
     * and one for Jan 23.
     * 
     * @param year the year of the requested pay data
     * @param period_no the period number of the requested pay data
     * @param filters user-provided input that determines which company and types are exported
     * @returns The desired filtered data in semi-colon-separated format, grouped and sorted by 
     * employee and by bank codes.
     */
    async collectTransaction(
        year: number,
        period_no: number,
        filters: Filters
    ): Promise<CsvRow[]> {
        const BILLABLE_SHIFT_TYPES: BillableShiftType[] = [];

        if (filters.types.shifts) BILLABLE_SHIFT_TYPES.push('REGULAR', 'OVERTIME', 'EMERGENCY', 'EVENING', 'SICK');
        if (filters.types.holiday) BILLABLE_SHIFT_TYPES.push('HOLIDAY');
        if (filters.types.vacation) BILLABLE_SHIFT_TYPES.push('VACATION');

        const BILLABLE_SHIFT_CODES = await this.resolveShiftTypeCode(BILLABLE_SHIFT_TYPES);
        const PTO_SHIFT_CODES = await this.resolveShiftTypeCode(['VACATION', 'SICK', 'HOLIDAY']);
        const HOLIDAY_SHIFT_CODE = await this.resolveShiftTypeCode(['HOLIDAY']);

        const period = await this.prisma.payPeriods.findFirst({
            where: { pay_year: year, pay_period_no: period_no },
            select: { period_start: true, period_end: true },
        });
        if (!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`);

        const start = period.period_start;
        const end = period.period_end;

        const company_codes = resolveCompanyCodes(filters.companies);
        if (company_codes.length === 0) throw new BadRequestException('NO_COMPANY_SELECTED');

        if (Object.values(filters.types).every(type => type === false))
            throw new BadRequestException('NO_TYPE_SELECTED');

        const exportedShifts = await this.prisma.shifts.findMany({
            where: {
                date: { gte: start, lte: end },
                is_approved: true,
                timesheet: { employee: { company_code: { in: company_codes } } },
                bank_code: { bank_code: { in: BILLABLE_SHIFT_CODES } },
            },
            select: select_csv_shift_lines,
        });

        const rows: InternalCsvRow[] = exportedShifts.map(shift => {
            const employee = shift.timesheet.employee;
            const week = computeWeekNumber(start, shift.date);
            const type_transaction = shift.bank_code.bank_code.charAt(0);
            const code = Number(shift.bank_code.bank_code.slice(1,));
            const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code);

            return {
                timesheet_id: shift.timesheet.id,
                shift_date: shift.date,
                compagnie_no: employee.company_code,
                employee_matricule: employee.external_payroll_id,
                releve: 0,
                type_transaction: type_transaction,
                code: code,
                quantite_hre: computeHours(shift.start_time, shift.end_time),
                taux_horaire: '',
                montant: undefined,
                semaine_no: week,
                division_no: undefined,
                service_no: undefined,
                departem_no: undefined,
                sous_departem_no: undefined,
                date_transaction: formatDate(end),
                premier_jour_absence: isPTO ? formatDate(shift.date) : '',
                dernier_jour_absence: isPTO ? formatDate(shift.date) : '',
            }
        });

        if (filters.types.expenses) {
            const exportedExpenses = await this.prisma.expenses.findMany({
                where: {
                    date: { gte: start, lte: end },
                    is_approved: true,
                    timesheet: { employee: { company_code: { in: company_codes } } },
                },
                select: select_csv_expense_lines,
            });

            exportedExpenses.map(expense => {
                const employee = expense.timesheet.employee;
                const type_transaction = expense.bank_code.bank_code.charAt(0);
                const code = Number(expense.bank_code.bank_code.slice(1,))
                const week = computeWeekNumber(start, expense.date);
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                const amount = expense.bank_code.bank_code === 'G503' ? expense.mileage : expense.amount;
                const adjustedAmount = Number(amount) * expense.bank_code.modifier;

                rows.push({
                    timesheet_id: expense.timesheet.id,
                    shift_date: expense.date,
                    compagnie_no: employee.company_code,
                    employee_matricule: employee.external_payroll_id,
                    releve: 0,
                    type_transaction: type_transaction,
                    code: code,
                    quantite_hre: undefined,
                    taux_horaire: undefined,
                    montant: adjustedAmount,
                    semaine_no: week,
                    division_no: undefined,
                    service_no: undefined,
                    departem_no: undefined,
                    sous_departem_no: undefined,
                    date_transaction: formatDate(end),
                    premier_jour_absence: undefined,
                    dernier_jour_absence: undefined,
                });
            });
        }

        // Sort shifts and expenses according to their bank codes
        rows.sort((a, b) =>
            a.code - b.code ||
            a.employee_matricule - b.employee_matricule
        );

        const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]);
        const consolidated_rows = consolidateRowHoursAndAmountByType(holiday_rows);
        //requalifies regular hours into overtime when needed
        const requalified_rows = applyOvertimeRequalifications(consolidated_rows);

        return requalified_rows;
    }

    resolveShiftTypeCode = async (shift_type: BillableShiftType[]): Promise<string[]> => {
        const billableBankCodes = await this.prisma.bankCodes.findMany({
            where: { type: { in: shift_type } },
            select: {
                bank_code: true,
                shifts: {
                    select: {
                        date: true,
                    },
                },
            },
        });
        if (!billableBankCodes) throw new BadRequestException('Missing Shift bank code');

        const shiftCodes: string[] = [];
        billableBankCodes.map(billableBankCode => shiftCodes.push(billableBankCode.bank_code));
        return shiftCodes;
    }

}
