<?php

namespace App\Support;

use App\Models\AttendanceLeaveApplication;
use App\Models\AttendancePunch;
use App\Models\PermanentEmployee;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod;
use Morilog\Jalali\Jalalian;

/**
 * Shared Shamsi (Solar Hijri) month window for attendance records and salary absent-day counts.
 */
final class ShamsiMonthAttendance
{
    /**
     * @return array{
     *     salary_month: string,
     *     date_from: string,
     *     date_to: string,
     *     shamsi_label: string,
     *     jalali_year: int,
     *     jalali_month: int,
     * }
     */
    public static function filterDates(CarbonInterface|string|null $salaryMonthAnchor = null): array
    {
        $start = SalaryMonth::normalizeStart($salaryMonthAnchor);
        $period = SalaryMonth::period($start);
        $lastDay = self::lastCountableDay($start);

        return [
            'salary_month' => SalaryMonth::urlKey($start),
            'date_from' => $period['start']->toDateString(),
            'date_to' => $lastDay->toDateString(),
            'shamsi_label' => SalaryMonth::displayLabel($start),
            'jalali_year' => $period['jalali_year'],
            'jalali_month' => $period['jalali_month'],
        ];
    }

    public static function lastCountableDay(CarbonInterface|string $salaryMonthAnchor): Carbon
    {
        $monthEnd = SalaryMonth::period($salaryMonthAnchor)['end'];
        $today = Carbon::now()->startOfDay();
        $end = Carbon::parse($monthEnd)->startOfDay();

        if ($end->lt($today)) {
            return $end;
        }

        return $today->copy()->subDay();
    }

    public static function firstEmployedDay(PermanentEmployee $employee, Carbon $monthStart): Carbon
    {
        $firstDay = $monthStart->copy();

        if ($employee->joining_date !== null) {
            $joined = Carbon::parse($employee->joining_date->format('Y-m-d'))->startOfDay();

            if ($joined->gt($firstDay)) {
                $firstDay = $joined;
            }
        }

        return $firstDay;
    }

    /**
     * @param  list<int>  $employeeIds
     * @return array<int, int>
     */
    public static function absentDayCountsForEmployees(
        array $employeeIds,
        CarbonInterface|string $salaryMonthAnchor,
    ): array {
        if ($employeeIds === []) {
            return [];
        }

        $period = SalaryMonth::period($salaryMonthAnchor);
        $monthStart = $period['start'];
        $lastDay = self::lastCountableDay($salaryMonthAnchor);

        $employees = PermanentEmployee::query()
            ->whereIn('id', $employeeIds)
            ->get(['id', 'joining_date']);

        $leaveByStaffDate = self::leaveMap($monthStart, $lastDay, $employeeIds);
        $punchesByStaffDate = self::punchMap($monthStart, $lastDay, $employeeIds);

        $counts = [];

        foreach ($employees as $employee) {
            $staffKey = StaffAttendance::staffKey(StaffType::PERMANENT, $employee->id);
            $firstDay = self::firstEmployedDay($employee, $monthStart);

            if ($firstDay->gt($lastDay)) {
                $counts[$employee->id] = 0;

                continue;
            }

            $counts[$employee->id] = self::countAbsentDaysWithActivity(
                $staffKey,
                $firstDay,
                $lastDay,
                $leaveByStaffDate,
                $punchesByStaffDate,
            );
        }

        return $counts;
    }

    /**
     * Absent days visible in attendance records (days with punch or leave activity only).
     */
    public static function absentDayCountFromAttendanceRecords(
        PermanentEmployee $employee,
        CarbonInterface|string $salaryMonthAnchor,
    ): int {
        $period = SalaryMonth::period($salaryMonthAnchor);
        $monthStart = $period['start'];
        $lastDay = self::lastCountableDay($salaryMonthAnchor);
        $firstDay = self::firstEmployedDay($employee, $monthStart);

        if ($firstDay->gt($lastDay)) {
            return 0;
        }

        $staffKey = StaffAttendance::staffKey(StaffType::PERMANENT, $employee->id);
        $leaveByStaffDate = self::leaveMap($monthStart, $lastDay, [$employee->id]);
        $punchesByStaffDate = self::punchMap($monthStart, $lastDay, [$employee->id]);

        return self::countAbsentDaysWithActivity(
            $staffKey,
            $firstDay,
            $lastDay,
            $leaveByStaffDate,
            $punchesByStaffDate,
        );
    }

    /**
     * Count absent days that appear in attendance records (punch or leave activity only).
     *
     * @param  array<string, array<string, string>>  $leaveByStaffDate
     * @param  array<string, array<string, array{check_in: string|null, check_out: string|null}>>  $punchesByStaffDate
     */
    public static function countAbsentDaysWithActivity(
        string $staffKey,
        Carbon $firstDay,
        Carbon $lastDay,
        array $leaveByStaffDate,
        array $punchesByStaffDate,
    ): int {
        $dateKeys = array_unique(array_merge(
            array_keys($leaveByStaffDate[$staffKey] ?? []),
            array_keys($punchesByStaffDate[$staffKey] ?? []),
        ));

        $absentDays = 0;

        foreach ($dateKeys as $dateKey) {
            $day = Carbon::parse($dateKey)->startOfDay();

            if ($day->lt($firstDay) || $day->gt($lastDay)) {
                continue;
            }

            if (self::dayStatus($staffKey, $dateKey, $leaveByStaffDate, $punchesByStaffDate) === AttendanceDayStatus::ABSENT) {
                $absentDays++;
            }
        }

        return $absentDays;
    }

    public static function shamsiLabelForDate(CarbonInterface|string $gregorianDate): string
    {
        $carbon = Carbon::parse($gregorianDate)->startOfDay();
        $jalali = Jalalian::fromCarbon($carbon);

        return $jalali->getYear().'/'.str_pad((string) $jalali->getMonth(), 2, '0', STR_PAD_LEFT);
    }

    /**
     * @param  array<string, array<string, string>>  $leaveByStaffDate
     * @param  array<string, array<string, array{check_in: string|null, check_out: string|null}>>  $punchesByStaffDate
     */
    public static function dayStatus(
        string $staffKey,
        string $dateKey,
        array $leaveByStaffDate,
        array $punchesByStaffDate,
    ): string {
        $leaveType = $leaveByStaffDate[$staffKey][$dateKey] ?? null;
        $dayPunches = $punchesByStaffDate[$staffKey][$dateKey] ?? [
            'check_in' => null,
            'check_out' => null,
        ];

        return AttendanceDayStatus::resolve(
            $leaveType,
            $dayPunches['check_in'] !== null,
            $dayPunches['check_out'] !== null,
        );
    }

    /**
     * @param  list<int>  $employeeIds
     * @return array<string, array<string, string>>
     */
    private static function leaveMap(Carbon $start, Carbon $lastDay, array $employeeIds): array
    {
        $map = [];

        $query = AttendanceLeaveApplication::query()
            ->where('staff_type', StaffType::PERMANENT)
            ->whereIn('staff_id', $employeeIds)
            ->whereDate('from_date', '<=', $lastDay)
            ->whereDate('to_date', '>=', $start);

        foreach ($query->get(['staff_type', 'staff_id', 'leave_type', 'from_date', 'to_date']) as $leave) {
            $staffKey = StaffAttendance::staffKey($leave->staff_type, $leave->staff_id);
            $periodStart = Carbon::parse($leave->from_date)->max($start);
            $periodEnd = Carbon::parse($leave->to_date)->min($lastDay);

            foreach (CarbonPeriod::create($periodStart, $periodEnd) as $day) {
                $map[$staffKey][$day->toDateString()] = $leave->leave_type;
            }
        }

        return $map;
    }

    /**
     * @param  list<int>  $employeeIds
     * @return array<string, array<string, array{check_in: string|null, check_out: string|null}>>
     */
    private static function punchMap(Carbon $start, Carbon $lastDay, array $employeeIds): array
    {
        $map = [];

        $query = AttendancePunch::query()
            ->where('staff_type', StaffType::PERMANENT)
            ->whereIn('staff_id', $employeeIds)
            ->whereDate('punched_at', '>=', $start)
            ->whereDate('punched_at', '<=', $lastDay);

        foreach ($query->orderBy('punched_at')->get(['staff_type', 'staff_id', 'punched_at', 'punch_type']) as $punch) {
            $staffKey = StaffAttendance::staffKey($punch->staff_type, $punch->staff_id);
            $dateKey = $punch->punched_at->toDateString();

            if (! isset($map[$staffKey][$dateKey])) {
                $map[$staffKey][$dateKey] = [
                    'check_in' => null,
                    'check_out' => null,
                ];
            }

            $punchType = AttendancePunchType::normalize($punch->punch_type);

            if ($punchType === AttendancePunchType::CHECK_IN) {
                if ($map[$staffKey][$dateKey]['check_in'] === null) {
                    $map[$staffKey][$dateKey]['check_in'] = $punch->punched_at->toIso8601String();
                }
            } elseif ($punchType === AttendancePunchType::CHECK_OUT
                && ($map[$staffKey][$dateKey]['check_out'] === null
                    || $punch->punched_at->toIso8601String() > $map[$staffKey][$dateKey]['check_out'])) {
                $map[$staffKey][$dateKey]['check_out'] = $punch->punched_at->toIso8601String();
            }
        }

        return $map;
    }
}
