<?php

namespace App\Services\Admin\HR;

use App\Concerns\HasRoleChecks;
use App\Enums\Role;
use App\Models\Attendance;
use App\Models\Employee;
use App\Models\LeaveRequest;
use App\Models\User;
use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService;
use App\Services\S3PresignedService;
use App\Settings\HRSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\ValidationException;

class AttendanceService
{
    use HasRoleChecks, RegistersMedia;

    public function __construct(
        private S3PresignedService $s3Service,
        private EmployeeService $employeeService,
    ) {}

    public function getIndexData(int $year, int $month): array
    {
        $user = auth()->user();
        $isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER]);
        $hrSettings = app(HRSettings::class);

        $employeeId = $isAdmin ? null : $user->employee?->id;

        return [
            'attendances' => $this->getByMonth($year, $month, $employeeId),
            'leaves' => $this->getLeavesByMonth($year, $month, $employeeId),
            'employees' => $isAdmin ? $this->employeeService->getAll() : [],
            'todayAttendance' => $isAdmin ? null : $this->getToday(),
            'currentYear' => $year,
            'currentMonth' => $month,
            'monthStats' => $this->getMonthStats($year, $month, $employeeId),
            'hrSettings' => [
                'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
                'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
            ],
            'isOnLeave' => $isAdmin ? false : $this->isOnLeave($user),
            'canCheckIn' => $isAdmin ? false : $user->employee !== null,
            'isAdmin' => $isAdmin,
        ];
    }

    public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection
    {
        return Attendance::with(['employee.user.userProfile', 'media'])
            ->whereYear('attendance_date', $year)
            ->whereMonth('attendance_date', $month)
            ->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId))
            ->get()
            ->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
    }

    public function getByDate(string $date): ?array
    {
        $attendance = Attendance::with(['employee.user.userProfile', 'media'])
            ->where('attendance_date', $date)
            ->where('employee_id', auth()->user()->employee?->id)
            ->first();

        return $attendance ? $this->formatAttendance($attendance) : null;
    }

    public function getToday(): ?array
    {
        return $this->getByDate(now()->toDateString());
    }

    public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null): Collection
    {
        $startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
        $endOfMonth = $startOfMonth->copy()->endOfMonth();

        return LeaveRequest::approved()
            ->with('employee.user.userProfile')
            ->where('start_date', '<=', $endOfMonth)
            ->where('end_date', '>=', $startOfMonth)
            ->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId))
            ->get()
            ->map(fn (LeaveRequest $leave) => [
                'id' => $leave->id,
                'employee_id' => $leave->employee_id,
                'start_date' => $leave->start_date->toDateString(),
                'end_date' => $leave->end_date->toDateString(),
                'total_days' => $leave->total_days,
                'status' => $leave->status->value,
                'employee_name' => $leave->employee?->user?->userProfile?->full_name ?? '-',
            ]);
    }

    public function getMonthStats(int $year, int $month, ?int $employeeId = null): array
    {
        $startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
        $endOfMonth = $startOfMonth->copy()->endOfMonth();
        $today = Carbon::today();
        $statEnd = $endOfMonth->lte($today) ? $endOfMonth : $today;

        $workingDays = 0;
        $current = $startOfMonth->copy();
        while ($current->lte($statEnd)) {
            if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
                $workingDays++;
            }
            $current->addDay();
        }

        if ($employeeId) {
            return $this->getMonthStatsForEmployee($year, $month, $startOfMonth, $statEnd, $workingDays, $employeeId);
        }

        return $this->getMonthStatsForAll($year, $month, $startOfMonth, $statEnd, $workingDays);
    }

    public function isOnLeave(User $user): bool
    {
        $employee = $user->employee;

        if (! $employee) {
            return false;
        }

        return LeaveRequest::approved()
            ->where('employee_id', $employee->id)
            ->where('start_date', '<=', now()->toDateString())
            ->where('end_date', '>=', now()->toDateString())
            ->exists();
    }

    public function checkIn(array $data): Attendance
    {
        $employee = auth()->user()->employee;

        if (! $employee) {
            throw ValidationException::withMessages([
                'employee' => 'Anda tidak terdaftar sebagai karyawan.',
            ]);
        }

        $today = now()->toDateString();

        $existing = Attendance::where('employee_id', $employee->id)
            ->where('attendance_date', $today)
            ->first();

        if ($existing) {
            throw ValidationException::withMessages([
                'attendance' => 'Anda sudah melakukan presensi hari ini.',
            ]);
        }

        $attendance = Attendance::create([
            'employee_id' => $employee->id,
            'attendance_date' => $today,
            'check_in_at' => now(),
            'check_in_latitude' => $data['latitude'],
            'check_in_longitude' => $data['longitude'],
        ]);

        if (! empty($data['photo'])) {
            $this->registerMediaFromBase64($attendance, $data['photo'], 'checkin', 'attendances');
        }

        NotificationService::notify(
            roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
            title: 'Presensi Masuk',
            body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
            url: route('admin.hr.attendances.index'),
        );

        return $attendance;
    }

    public function checkOut(Attendance $attendance, array $data): Attendance
    {
        $attendance->update([
            'check_out_at' => now(),
            'check_out_latitude' => $data['latitude'],
            'check_out_longitude' => $data['longitude'],
        ]);

        if (! empty($data['photo'])) {
            $this->registerMediaFromBase64($attendance, $data['photo'], 'checkout', 'attendances');
        }

        NotificationService::notify(
            roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
            title: 'Presensi Pulang',
            body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
            url: route('admin.hr.attendances.index'),
        );

        return $attendance;
    }

    private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays, int $employeeId): array
    {
        $attendanceQuery = Attendance::whereYear('attendance_date', $year)
            ->whereMonth('attendance_date', $month)
            ->where('attendance_date', '<=', $statEnd->toDateString())
            ->where('employee_id', $employeeId);

        $attendanceDates = $attendanceQuery->pluck('attendance_date')
            ->map(fn ($d) => Carbon::parse($d)->toDateString())
            ->filter(fn ($d) => ! in_array(Carbon::parse($d)->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY]))
            ->unique()
            ->values();
        $attendanceCount = $attendanceDates->count();

        $leaveQuery = LeaveRequest::approved()
            ->where('start_date', '<=', $statEnd)
            ->where('end_date', '>=', $startOfMonth)
            ->where('employee_id', $employeeId);

        $leaveRequests = $leaveQuery->get();
        $leaveCount = $leaveRequests->count();

        $leaveDates = collect();
        $leaveRequests->each(function ($leave) use (&$leaveDates, $startOfMonth, $statEnd) {
            $leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay();
            $leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay();
            $current = $leaveStart->copy();
            while ($current->lte($leaveEnd)) {
                if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
                    $leaveDates->push($current->toDateString());
                }
                $current->addDay();
            }
        });
        $leaveDates = $leaveDates->unique()->values();

        $coveredDates = $attendanceDates->merge($leaveDates)->unique()->count();
        $absent = max(0, $workingDays - $coveredDates);

        return [
            'working_days' => $workingDays,
            'present' => $attendanceCount,
            'absent' => $absent,
            'leave' => $leaveCount,
        ];
    }

    private function getMonthStatsForAll(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays): array
    {
        $totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count();

        $presentCount = Attendance::whereYear('attendance_date', $year)
            ->whereMonth('attendance_date', $month)
            ->where('attendance_date', '<=', $statEnd->toDateString())
            ->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true)))
            ->count();

        $leaveRequests = LeaveRequest::approved()
            ->where('start_date', '<=', $statEnd)
            ->where('end_date', '>=', $startOfMonth)
            ->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true)))
            ->get();

        $leaveDays = 0;
        $leaveRequests->each(function ($leave) use (&$leaveDays, $startOfMonth, $statEnd) {
            $leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay();
            $leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay();
            $current = $leaveStart->copy();
            while ($current->lte($leaveEnd)) {
                if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
                    $leaveDays++;
                }
                $current->addDay();
            }
        });

        $absent = max(0, ($totalEmployees * $workingDays) - $presentCount - $leaveDays);

        return [
            'working_days' => $workingDays,
            'present' => $presentCount,
            'absent' => $absent,
            'leave' => $leaveDays,
            'total_employees' => $totalEmployees,
        ];
    }

    private function formatAttendance(Attendance $attendance): array
    {
        $toArray = $attendance->toArray();
        $toArray['employee_name'] = $attendance->employee?->user?->userProfile?->full_name ?? '-';

        if (is_null($toArray['work_duration_minutes']) && $attendance->check_in_at) {
            $checkIn = Carbon::parse($attendance->check_in_at);
            $end = $checkIn->toDateString() === now()->toDateString()
                ? now()
                : $checkIn->copy()->endOfDay();
            $toArray['work_duration_minutes'] = $checkIn->diffInMinutes($end);
        }

        $checkInMedia = $attendance->getFirstMedia('checkin');
        $checkOutMedia = $attendance->getFirstMedia('checkout');

        $toArray['check_in_photo'] = $checkInMedia
            ? Cache::remember("attendance_check_in_{$checkInMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkInMedia->getPath()))
            : null;

        $toArray['check_out_photo'] = $checkOutMedia
            ? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkOutMedia->getPath()))
            : null;

        return $toArray;
    }
}
