<?php

namespace App\Modules\HR\Shifts\Services;

use App\Modules\HR\Employees\Models\Employee;
use App\Modules\HR\Shifts\DTOs\ShiftTimeContext;
use App\Modules\HR\Shifts\Models\Shift;
use App\Modules\HR\Shifts\Repositories\EmployeeShiftAssignmentRepository;
use App\Modules\HR\Shifts\Support\ShiftScheduleEndpoints;
use App\Modules\HR\Shifts\Support\ShiftScheduleMinutes;
use App\Modules\HR\Shifts\Support\ShiftWeekday;
use Carbon\CarbonImmutable;

class ShiftResolutionService
{
    public function __construct(
        private readonly EmployeeShiftAssignmentRepository $assignments,
    ) {}

    public function resolveAttendanceDateForPunch(Employee $employee, CarbonImmutable $punch): string
    {
        $a = $this->assignments->coveringInstant((int) $employee->tenant_id, (int) $employee->id, $punch);
        $shift = $a?->shift;
        if ($shift === null) {
            return $punch->toDateString();
        }

        return $this->resolveDayWithinWindows($shift, $punch);
    }

    public function resolveContextForAttendanceDate(
        Employee $employee,
        string $attendanceDateYmd,
        CarbonImmutable $checkIn,
    ): ?ShiftTimeContext {
        $day = CarbonImmutable::parse($attendanceDateYmd)->startOfDay();
        $a = $this->assignments->coveringDate((int) $employee->tenant_id, (int) $employee->id, $day);
        $shift = $a?->shift;
        if ($shift === null) {
            return null;
        }
        if (! $this->isWorkingCalendarDay($shift, $day)) {
            return null;
        }
        $start = ShiftScheduleEndpoints::shiftStartOnDate($shift, $day);
        $threshold = $start->addMinutes((int) $shift->grace_minutes + (int) $shift->late_after_minutes);
        $workMin = ShiftScheduleMinutes::scheduledWorkMinutes($shift);
        $half = min(
            (float) config('attendance.half_day_max_hours', 4.5),
            max(2.0, ($workMin / 60) * 0.5)
        );

        return new ShiftTimeContext(
            shift: $shift,
            latenessThreshold: $threshold,
            halfDayMaxHours: $half,
            scheduledWorkMinutes: $workMin,
            overtimeAfterMinutes: (int) $shift->overtime_after_minutes,
        );
    }

    public function checkInWithinExpectedWindows(
        int $tenantId,
        int $employeeId,
        string $attendanceDateYmd,
        CarbonImmutable $checkIn,
    ): bool {
        $day = CarbonImmutable::parse($attendanceDateYmd)->startOfDay();
        $a = $this->assignments->coveringDate($tenantId, $employeeId, $day);
        $shift = $a?->shift;
        if ($shift === null) {
            return true;
        }
        for ($i = -1; $i <= 1; $i++) {
            $d = $day->addDays($i);
            if ($this->punchInsideWindow($shift, $d, $checkIn)) {
                return true;
            }
        }

        return false;
    }

    public function violatesAssignedShiftWindow(
        int $tenantId,
        int $employeeId,
        string $attendanceDateYmd,
        CarbonImmutable $checkIn,
    ): bool {
        $day = CarbonImmutable::parse($attendanceDateYmd)->startOfDay();
        $a = $this->assignments->coveringDate($tenantId, $employeeId, $day);
        $shift = $a?->shift;
        if ($shift === null) {
            return false;
        }
        if (! $this->isWorkingCalendarDay($shift, $day)) {
            return true;
        }

        return ! $this->checkInWithinExpectedWindows($tenantId, $employeeId, $attendanceDateYmd, $checkIn);
    }

    private function resolveDayWithinWindows(Shift $shift, CarbonImmutable $punch): string
    {
        $base = $punch->startOfDay();
        for ($i = -1; $i <= 1; $i++) {
            $d = $base->addDays($i);
            if ($this->punchInsideWindow($shift, $d, $punch)) {
                return $d->toDateString();
            }
        }

        return $punch->toDateString();
    }

    private function punchInsideWindow(Shift $shift, CarbonImmutable $shiftCalendarDay, CarbonImmutable $punch): bool
    {
        if (! $this->isWorkingCalendarDay($shift, $shiftCalendarDay)) {
            return false;
        }
        $s = ShiftScheduleEndpoints::shiftStartOnDate($shift, $shiftCalendarDay);
        $e = ShiftScheduleEndpoints::shiftEndOnDate($shift, $shiftCalendarDay);

        return ! $punch->lt($s) && ! $punch->gt($e);
    }

    private function isWorkingCalendarDay(Shift $shift, CarbonImmutable $day): bool
    {
        $key = $this->weekdayKey($day);
        $list = array_map('strtolower', $shift->working_days ?? []);

        return in_array(strtolower($key), $list, true);
    }

    private function weekdayKey(CarbonImmutable $day): string
    {
        return match ((int) $day->dayOfWeekIso) {
            1 => ShiftWeekday::MONDAY,
            2 => ShiftWeekday::TUESDAY,
            3 => ShiftWeekday::WEDNESDAY,
            4 => ShiftWeekday::THURSDAY,
            5 => ShiftWeekday::FRIDAY,
            6 => ShiftWeekday::SATURDAY,
            7 => ShiftWeekday::SUNDAY,
            default => ShiftWeekday::MONDAY,
        };
    }
}
