<?php

namespace App\Modules\HR\Attendance\Services;

use App\Modules\HR\Attendance\Jobs\RunAttendanceExceptionDetectionJob;
use App\Modules\HR\Attendance\Models\BiometricDevice;
use App\Modules\HR\Attendance\Models\BiometricLog;
use App\Modules\HR\Attendance\Repositories\AttendanceRecordRepository;
use App\Modules\HR\Attendance\Repositories\BiometricDeviceRepository;
use App\Modules\HR\Attendance\Repositories\BiometricLogRepository;
use App\Modules\HR\Attendance\Support\AttendanceRecordStatus;
use App\Modules\HR\Attendance\Support\AttendanceSource;
use App\Modules\HR\Employees\Models\Employee;
use App\Modules\HR\Shifts\Services\ShiftResolutionService;
use App\Modules\HR\Shifts\Support\ShiftOvertimePreview;
use App\Modules\Payroll\Services\PayrollProtectionService;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;

class AttendanceProcessorService
{
    public function __construct(
        private readonly BiometricLogRepository $logs,
        private readonly BiometricDeviceRepository $devices,
        private readonly AttendanceRecordRepository $records,
        private readonly AttendanceLateStatusResolver $lateResolver,
        private readonly ShiftResolutionService $shifts,
        private readonly BiometricAttendanceDayClusterer $clusterer,
        private readonly PayrollProtectionService $payrollProtection,
    ) {}

    public function processPendingForDevice(int $deviceId): int
    {
        $device = $this->devices->findById($deviceId);
        if ($device === null || ! $device->is_active) {
            return 0;
        }
        $tenantId = (int) $device->tenant_id;
        $batch = $this->logs->pendingForDevice($tenantId, $deviceId, 800);
        if ($batch->isEmpty()) {
            return 0;
        }
        $processed = 0;
        foreach ($batch->groupBy('employee_fingerprint_id') as $group) {
            foreach ($this->clusterer->clusterByTimeGaps($group) as $cluster) {
                $this->processCluster($tenantId, $device, $cluster);
                $processed += $cluster->count();
            }
        }
        $this->devices->touchLastSync($device);

        return $processed;
    }

    /**
     * @param  Collection<int, BiometricLog>  $sortedCluster
     */
    private function processCluster(int $tenantId, BiometricDevice $device, Collection $sortedCluster): void
    {
        $sorted = $sortedCluster->sortBy(fn (BiometricLog $l) => $l->punch_time->timestamp)->values();
        $first = $sorted->first();
        if ($first === null) {
            return;
        }
        $fp = $first->employee_fingerprint_id;
        $employee = Employee::query()
            ->where('tenant_id', $tenantId)
            ->where('fingerprint_id', $fp)
            ->first();
        if ($employee === null) {
            foreach ($sorted as $log) {
                $this->logs->markProcessed($log);
            }

            return;
        }
        $checkIn = $sorted->first()->punch_time;
        $checkOut = $sorted->count() > 1 ? $sorted->last()->punch_time : null;
        $in = CarbonImmutable::instance($checkIn);
        $attendanceDate = $this->shifts->resolveAttendanceDateForPunch($employee, $in);
        $existing = $this->records->findForEmployeeDate((int) $employee->id, $attendanceDate);
        if ($existing !== null && $existing->source === AttendanceSource::LEAVE) {
            foreach ($sorted as $log) {
                $this->logs->markProcessed($log);
            }

            return;
        }
        $this->payrollProtection->ensureDateWritable(
            $tenantId,
            $attendanceDate->toDateString(),
            null,
        );
        foreach ($sorted as $log) {
            $this->logs->markProcessed($log);
        }
        $ctx = $this->shifts->resolveContextForAttendanceDate($employee, $attendanceDate, $in);
        if ($ctx !== null) {
            $threshold = $ctx->latenessThreshold;
            $half = $ctx->halfDayMaxHours;
        } else {
            $expected = (string) config('attendance.expected_start', '09:00');
            $grace = (int) config('attendance.late_grace_minutes', 0);
            $threshold = CarbonImmutable::parse($in->toDateString().' '.$expected, $in->getTimezone())->addMinutes($grace);
            $half = (float) config('attendance.half_day_max_hours', 4.5);
        }
        if ($checkOut === null) {
            $status = $in->gt($threshold) ? AttendanceRecordStatus::LATE : AttendanceRecordStatus::PRESENT;
        } else {
            $status = $this->lateResolver->resolve(
                $in,
                CarbonImmutable::instance($checkOut),
                $threshold,
                $half,
            );
        }
        $notes = 'Device '.$device->name;
        $overtimeMinutes = 0;
        if ($ctx !== null && $checkOut !== null) {
            $ot = ShiftOvertimePreview::minutesPastOvertimeThreshold(
                $ctx->shift,
                $attendanceDate,
                CarbonImmutable::instance($checkOut),
            );
            if ($ot > 0) {
                $overtimeMinutes = $ot;
                $notes .= ' | OT '.$ot.'m';
            }
        }
        $this->records->updateOrCreateDay($tenantId, (int) $employee->id, $attendanceDate, [
            'check_in_at' => $checkIn,
            'check_out_at' => $checkOut,
            'overtime_minutes' => $overtimeMinutes,
            'missing_checkout' => $checkOut === null,
            'source' => AttendanceSource::BIOMETRIC,
            'status' => $status,
            'notes' => $notes,
            'updated_by' => null,
        ]);
        RunAttendanceExceptionDetectionJob::dispatch($tenantId, $attendanceDate, $attendanceDate);
    }
}
