<?php

namespace App\Modules\HR\Attendance\Services;

use App\Models\User;
use App\Modules\Admin\Tenancy\Models\Tenant;
use App\Modules\HR\Attendance\Jobs\RunAttendanceExceptionDetectionJob;
use App\Modules\HR\Attendance\Models\AttendanceRecord;
use App\Modules\HR\Attendance\Notifications\MissingCheckoutReminderNotification;
use App\Modules\HR\Attendance\Repositories\AttendanceRecordRepository;
use App\Modules\HR\Attendance\Repositories\AttendanceReminderRepository;
use App\Modules\HR\Attendance\Support\AttendanceSource;
use App\Modules\HR\Employees\Models\Employee;
use App\Modules\HR\Shifts\Services\ShiftResolutionService;
use App\Modules\Payroll\Services\PayrollProtectionService;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class SelfAttendancePunchService
{
    public function __construct(
        private readonly AttendanceRecordRepository $records,
        private readonly AttendanceReminderRepository $reminders,
        private readonly AttendancePunchMetricsService $metrics,
        private readonly ShiftResolutionService $shifts,
        private readonly PayrollProtectionService $payrollProtection,
        private readonly AttendanceRecordActivityLogger $activityLog,
    ) {}

    public function checkIn(User $user, Tenant $tenant, Employee $employee): AttendanceRecord
    {
        $now = $this->nowFor($tenant);
        $date = $this->shifts->resolveAttendanceDateForPunch($employee, $now);
        $this->payrollProtection->ensureDateWritable((int) $tenant->id, $date, (int) $user->id);

        [$record, $rolledOver] = DB::transaction(function () use ($user, $tenant, $employee, $now, $date): array {
            $old = $this->rollOverOldRecords((int) $tenant->id, (int) $employee->id, $date, (int) $user->id);
            $today = $this->records->lockForEmployeeDate((int) $tenant->id, (int) $employee->id, $date);
            if ($today !== null) {
                $this->throwExistingRecordError($today);
            }
            $record = $this->records->updateOrCreateDay(
                (int) $tenant->id,
                (int) $employee->id,
                $date,
                [
                    'check_in_at' => $now,
                    'check_out_at' => null,
                    'overtime_minutes' => 0,
                    'missing_checkout' => false,
                    'source' => AttendanceSource::SELF_SERVICE,
                    'status' => $this->metrics->statusAtCheckIn($employee, $date, $now),
                    'notes' => null,
                    'created_by' => (int) $user->id,
                    'updated_by' => (int) $user->id,
                ],
            );

            return [$record, $old];
        });

        $this->activityLog->log(
            $record,
            'attendance_self_check_in',
            (int) $user->id,
            __('attendance.activity.self_check_in', ['date' => $date]),
        );
        $this->sendRolloverReminders($user, $rolledOver);

        return $record;
    }

    public function checkOut(User $user, Tenant $tenant, Employee $employee): AttendanceRecord
    {
        $now = $this->nowFor($tenant);
        $date = $this->shifts->resolveAttendanceDateForPunch($employee, $now);
        $this->payrollProtection->ensureDateWritable((int) $tenant->id, $date, (int) $user->id);

        $updated = DB::transaction(function () use ($user, $tenant, $employee, $now, $date): AttendanceRecord {
            $record = $this->records->lockForEmployeeDate((int) $tenant->id, (int) $employee->id, $date);
            if ($record === null || $record->check_in_at === null) {
                throw ValidationException::withMessages(['attendance' => [__('attendance.self.check_in_first')]]);
            }
            if ($record->check_out_at !== null) {
                throw ValidationException::withMessages(['attendance' => [__('attendance.self.already_checked_out')]]);
            }
            $checkIn = CarbonImmutable::instance($record->check_in_at);
            $metrics = $this->metrics->atCheckOut($employee, $date, $checkIn, $now);
            $saved = $this->records->update($record, [
                'check_out_at' => $now,
                'missing_checkout' => false,
                'status' => $metrics['status'],
                'overtime_minutes' => $metrics['overtime_minutes'],
                'updated_by' => (int) $user->id,
            ]);
            DB::afterCommit(fn () => RunAttendanceExceptionDetectionJob::dispatch(
                (int) $tenant->id,
                $date,
                $date,
            ));

            return $saved;
        });

        $this->activityLog->log(
            $updated,
            'attendance_self_check_out',
            (int) $user->id,
            __('attendance.activity.self_check_out', ['date' => $date]),
        );

        return $updated;
    }

    private function nowFor(Tenant $tenant): CarbonImmutable
    {
        return CarbonImmutable::now($tenant->timezone ?: config('app.timezone'));
    }

    private function rollOverOldRecords(int $tenantId, int $employeeId, string $date, int $userId): Collection
    {
        return $this->records->lockOpenBeforeDate($tenantId, $employeeId, $date)
            ->map(fn (AttendanceRecord $record) => $this->records->update($record, [
                'missing_checkout' => true,
                'updated_by' => $userId,
            ]));
    }

    private function sendRolloverReminders(User $user, Collection $records): void
    {
        foreach ($records as $record) {
            if (! $this->reminders->missingCheckoutReminderExists($user, (int) $record->id)) {
                $user->notify(new MissingCheckoutReminderNotification($record));
            }
        }
    }

    private function throwExistingRecordError(AttendanceRecord $record): never
    {
        $key = $record->check_out_at === null
            ? 'attendance.self.already_checked_in'
            : 'attendance.self.day_completed';
        throw ValidationException::withMessages(['attendance' => [__($key)]]);
    }
}
