<?php

namespace App\Modules\HR\Attendance\Repositories;

use App\Modules\HR\Attendance\Models\AttendanceRecord;
use App\Modules\HR\Attendance\Support\AttendanceRecordStatus;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;

class AttendanceRecordRepository
{
    public function paginateForTenant(int $tenantId, string $from, string $to, int $perPage = 30): LengthAwarePaginator
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->whereBetween('attendance_date', [$from, $to])
            ->with('employee')
            ->orderByDesc('attendance_date')
            ->paginate($perPage);
    }

    /**
     * @return LazyCollection<int, AttendanceRecord>
     */
    public function cursorForTenantDateRange(int $tenantId, string $from, string $to): LazyCollection
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->whereBetween('attendance_date', [$from, $to])
            ->with(['employee:id,tenant_id,full_name,employee_code'])
            ->orderBy('attendance_date')
            ->orderBy('id')
            ->cursor();
    }

    public function paginateForEmployee(
        int $tenantId,
        int $employeeId,
        string $from,
        string $to,
        int $perPage = 25,
    ): LengthAwarePaginator {
        return $this->paginateForEmployees($tenantId, [$employeeId], $from, $to, $perPage);
    }

    /**
     * @param  list<int>  $employeeIds
     */
    public function paginateForEmployees(
        int $tenantId,
        array $employeeIds,
        string $from,
        string $to,
        int $perPage = 25,
    ): LengthAwarePaginator {
        if ($employeeIds === []) {
            return AttendanceRecord::query()->whereRaw('0 = 1')->paginate($perPage);
        }

        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->whereIn('employee_id', $employeeIds)
            ->whereBetween('attendance_date', [$from, $to])
            ->with('employee:id,full_name,employee_code')
            ->orderByDesc('attendance_date')
            ->orderBy('employee_id')
            ->paginate($perPage);
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function updateOrCreateDay(int $tenantId, int $employeeId, string $date, array $data): AttendanceRecord
    {
        $data = array_merge([
            'tenant_id' => $tenantId,
            'employee_id' => $employeeId,
            'attendance_date' => $date,
        ], $data);

        return AttendanceRecord::query()->updateOrCreate(
            [
                'employee_id' => $employeeId,
                'attendance_date' => $date,
            ],
            $data,
        );
    }

    public function findForEmployeeDate(int $employeeId, string $date): ?AttendanceRecord
    {
        return AttendanceRecord::query()
            ->where('employee_id', $employeeId)
            ->whereDate('attendance_date', $date)
            ->first();
    }

    public function findForEmployeeDateInTenant(int $tenantId, int $employeeId, string $date): ?AttendanceRecord
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereDate('attendance_date', $date)
            ->first();
    }

    public function findOwnedByEmployee(int $tenantId, int $employeeId, int $recordId): ?AttendanceRecord
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereKey($recordId)
            ->first();
    }

    public function lockForEmployeeDate(int $tenantId, int $employeeId, string $date): ?AttendanceRecord
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereDate('attendance_date', $date)
            ->lockForUpdate()
            ->first();
    }

    /**
     * @return Collection<int, AttendanceRecord>
     */
    public function lockOpenBeforeDate(int $tenantId, int $employeeId, string $date): Collection
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereDate('attendance_date', '<', $date)
            ->whereNotNull('check_in_at')
            ->whereNull('check_out_at')
            ->lockForUpdate()
            ->get();
    }

    /**
     * @return Collection<int, AttendanceRecord>
     */
    public function unresolvedMissingCheckouts(int $tenantId, int $employeeId, int $limit = 10): Collection
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereNotNull('check_in_at')
            ->whereNull('check_out_at')
            ->where('missing_checkout', true)
            ->orderByDesc('attendance_date')
            ->limit($limit)
            ->get();
    }

    /**
     * @return LazyCollection<int, AttendanceRecord>
     */
    public function cursorOpenBeforeDateForTenant(int $tenantId, string $date): LazyCollection
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->whereDate('attendance_date', '<', $date)
            ->whereNotNull('check_in_at')
            ->whereNull('check_out_at')
            ->with('employee.user')
            ->orderBy('id')
            ->cursor();
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function update(AttendanceRecord $record, array $data): AttendanceRecord
    {
        $record->forceFill($data)->save();

        return $record->fresh(['employee']) ?? $record;
    }

    /**
     * @return Collection<int, AttendanceRecord>
     */
    public function forEmployeeInRange(int $tenantId, int $employeeId, string $from, string $to)
    {
        return AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereBetween('attendance_date', [$from, $to])
            ->orderBy('attendance_date')
            ->get();
    }

    /**
     * @return array{present: int, absent: int, late: int, leave: int, half_day: int}
     */
    public function countStatusesForTenantOnDate(int $tenantId, string $date): array
    {
        $rows = AttendanceRecord::query()
            ->select('status', DB::raw('COUNT(*) as c'))
            ->where('tenant_id', $tenantId)
            ->whereDate('attendance_date', $date)
            ->groupBy('status')
            ->pluck('c', 'status')
            ->all();

        return [
            'present' => (int) ($rows[AttendanceRecordStatus::PRESENT] ?? 0),
            'absent' => (int) ($rows[AttendanceRecordStatus::ABSENT] ?? 0),
            'late' => (int) ($rows[AttendanceRecordStatus::LATE] ?? 0),
            'leave' => (int) ($rows[AttendanceRecordStatus::LEAVE] ?? 0),
            'half_day' => (int) ($rows[AttendanceRecordStatus::HALF_DAY] ?? 0),
        ];
    }

    /**
     * @return array{present_days: int, absent_days: int, late_days: int, leave_days: int, overtime_hours: float, missing_checkouts: int}
     */
    /**
     * @return array{present: int, absent: int, late: int, leave: int, half_day: int, holiday: int, overtime_minutes: int}
     */
    public function payrollStatusCountsForEmployeeInRange(
        int $tenantId,
        int $employeeId,
        string $from,
        string $to,
    ): array {
        $rows = AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereBetween('attendance_date', [$from, $to])
            ->selectRaw('status, COUNT(*) as c')
            ->groupBy('status')
            ->pluck('c', 'status')
            ->all();

        $ot = (int) AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereBetween('attendance_date', [$from, $to])
            ->sum('overtime_minutes');

        return [
            'present' => (int) ($rows[AttendanceRecordStatus::PRESENT] ?? 0),
            'absent' => (int) ($rows[AttendanceRecordStatus::ABSENT] ?? 0),
            'late' => (int) ($rows[AttendanceRecordStatus::LATE] ?? 0),
            'leave' => (int) ($rows[AttendanceRecordStatus::LEAVE] ?? 0),
            'half_day' => (int) ($rows[AttendanceRecordStatus::HALF_DAY] ?? 0),
            'holiday' => (int) ($rows[AttendanceRecordStatus::HOLIDAY] ?? 0),
            'overtime_minutes' => $ot,
        ];
    }

    public function monthlyAggregatesForEmployee(int $tenantId, int $employeeId, CarbonImmutable $monthStart): array
    {
        $from = $monthStart->toDateString();
        $to = $monthStart->endOfMonth()->toDateString();
        $rows = AttendanceRecord::query()
            ->select('status', DB::raw('COUNT(*) as c'))
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereBetween('attendance_date', [$from, $to])
            ->groupBy('status')
            ->pluck('c', 'status')
            ->all();

        $otMinutes = AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereBetween('attendance_date', [$from, $to])
            ->sum('overtime_minutes');

        $missing = AttendanceRecord::query()
            ->where('tenant_id', $tenantId)
            ->where('employee_id', $employeeId)
            ->whereBetween('attendance_date', [$from, $to])
            ->where('missing_checkout', true)
            ->count();

        return [
            'present_days' => (int) (($rows[AttendanceRecordStatus::PRESENT] ?? 0) + ($rows[AttendanceRecordStatus::LATE] ?? 0)),
            'absent_days' => (int) ($rows[AttendanceRecordStatus::ABSENT] ?? 0),
            'late_days' => (int) ($rows[AttendanceRecordStatus::LATE] ?? 0),
            'leave_days' => (int) ($rows[AttendanceRecordStatus::LEAVE] ?? 0),
            'overtime_hours' => round(((int) $otMinutes) / 60, 2),
            'missing_checkouts' => (int) $missing,
        ];
    }
}
