<?php

namespace App\Services\Biometric;

use App\Models\Attendance;
use App\Models\AttendancePolicy;
use App\Models\AttendancePunch;
use App\Models\BiometricDevice;
use App\Models\Company;
use App\Models\User;
use App\Models\WorkDuration;
use App\Services\Attendance\AttendanceCalendarDayResolver;
use App\Services\Attendance\AttendancePolicyResolver;
use App\Services\CompanyResolver;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

class BiometricFirstInLastOutService
{
    public function __construct(
        protected CompanyResolver $companyResolver,
        protected AttendanceCalendarDayResolver $calendarDayResolver,
        protected AttendancePolicyResolver $policyResolver,
    ) {}

    public function processPunch(AttendancePunch $punch): AttendancePunch
    {
        $punch = AttendancePunch::withoutGlobalScopes()->findOrFail($punch->id);

        $company = Company::withoutGlobalScopes()->find($punch->company_id);
        if (! $company) {
            return $this->markError($punch, 'Company not found.');
        }

        $device = BiometricDevice::withoutGlobalScopes()->find($punch->biometric_device_id);
        if (! $device || ! $device->is_active) {
            return $this->markError($punch, 'Biometric device not found or inactive.');
        }

        if (($device->punch_mode ?: BiometricDevice::PUNCH_MODE_BOTH) !== BiometricDevice::PUNCH_MODE_BOTH) {
            return $punch;
        }

        $this->companyResolver->setCompany($company);
        $tz = $company->timezone ?: config('app.timezone', 'Asia/Kolkata');

        $rawDateTime = $punch->getAttributes()['punch_datetime']
            ?? ($punch->punch_datetime instanceof \DateTimeInterface
                ? $punch->punch_datetime->format('Y-m-d H:i:s')
                : (string) $punch->punch_datetime);
        $punchAt = Carbon::parse($rawDateTime, $tz);

        $user = $this->resolveEmployee($company, (string) $punch->emp_code);
        if (! $user) {
            return $this->markError(
                $punch,
                'No active employee found for emp_code (must match Employee ID).'
            );
        }

        $attendanceDate = $this->calendarDayResolver->resolveAttendanceDateForPunch($user, $punchAt);

        $punch->forceFill([
            'user_id' => $user->id,
            'attendance_date' => $attendanceDate,
            'source' => $punch->source ?: AttendancePunch::SOURCE_BIOMETRIC_DEVICE,
            'error_message' => null,
        ])->save();

        $this->recalculateEmployeeAttendance((int) $company->id, (int) $user->id, $attendanceDate);

        return AttendancePunch::withoutGlobalScopes()->findOrFail($punch->id);
    }

    public function recalculateEmployeeAttendance(int $companyId, int $userId, string $attendanceDate): void
    {
        $company = Company::withoutGlobalScopes()->find($companyId);
        $user = User::query()->find($userId);
        if (! $company || ! $user) {
            return;
        }

        $this->companyResolver->setCompany($company);
        $tz = $company->timezone ?: config('app.timezone', 'Asia/Kolkata');
        $gapMinutes = $this->minPunchGapMinutes($companyId);

        DB::transaction(function () use ($companyId, $userId, $attendanceDate, $company, $user, $tz, $gapMinutes) {
            $bothDeviceIds = BiometricDevice::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where(function ($q) {
                    $q->where('punch_mode', BiometricDevice::PUNCH_MODE_BOTH)
                        ->orWhereNull('punch_mode');
                })
                ->pluck('id')
                ->all();

            if ($bothDeviceIds === []) {
                return;
            }

            // Include legacy both-mode punches that predate attendance_date (toggle path).
            $existingAttendanceId = Attendance::query()
                ->where('company_id', $companyId)
                ->where('user_id', $userId)
                ->whereDate('date', $attendanceDate)
                ->value('id');

            $punches = AttendancePunch::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('user_id', $userId)
                ->whereIn('biometric_device_id', $bothDeviceIds)
                ->where('status', '!=', AttendancePunch::STATUS_ERROR)
                ->where(function ($q) use ($attendanceDate, $existingAttendanceId) {
                    $q->whereDate('attendance_date', $attendanceDate)
                        ->orWhereDate('punch_datetime', $attendanceDate)
                        ->orWhereDate('punch_date', $attendanceDate);
                    if ($existingAttendanceId) {
                        $q->orWhere('attendance_id', $existingAttendanceId);
                    }
                })
                ->orderBy('punch_datetime')
                ->orderBy('id')
                ->lockForUpdate()
                ->get();

            if ($punches->isEmpty()) {
                return;
            }

            // Stamp attendance_date so UI + future recalcs stay day-scoped.
            foreach ($punches as $punch) {
                if (! $punch->attendance_date || $punch->attendance_date->format('Y-m-d') !== $attendanceDate) {
                    $punch->forceFill(['attendance_date' => $attendanceDate])->save();
                }
            }

            $previousValidAt = null;
            $valid = [];

            foreach ($punches as $punch) {
                $raw = $punch->getAttributes()['punch_datetime']
                    ?? ($punch->punch_datetime instanceof \DateTimeInterface
                        ? $punch->punch_datetime->format('Y-m-d H:i:s')
                        : (string) $punch->punch_datetime);
                $at = Carbon::parse($raw, $tz);

                $isDuplicate = false;
                if ($previousValidAt !== null) {
                    $diffSeconds = $previousValidAt->diffInSeconds($at, false);
                    if ($diffSeconds >= 0 && $diffSeconds < ($gapMinutes * 60)) {
                        $isDuplicate = true;
                    }
                }

                if ($isDuplicate) {
                    $punch->forceFill([
                        'processed_type' => AttendancePunch::TYPE_DUPLICATE,
                        'is_duplicate' => true,
                        'status' => AttendancePunch::STATUS_APPLIED,
                        'error_message' => null,
                        'processed_at' => now(),
                    ])->save();
                    continue;
                }

                $previousValidAt = $at;
                $valid[] = ['punch' => $punch, 'at' => $at];
            }

            $count = count($valid);
            foreach ($valid as $index => $row) {
                /** @var AttendancePunch $punch */
                $punch = $row['punch'];
                if ($count === 1) {
                    $type = AttendancePunch::TYPE_FIRST_IN;
                } elseif ($index === 0) {
                    $type = AttendancePunch::TYPE_FIRST_IN;
                } elseif ($index === $count - 1) {
                    $type = AttendancePunch::TYPE_LAST_OUT;
                } else {
                    $type = AttendancePunch::TYPE_INTERMEDIATE;
                }

                $punch->forceFill([
                    'processed_type' => $type,
                    'is_duplicate' => false,
                    'status' => AttendancePunch::STATUS_APPLIED,
                    'error_message' => null,
                    'processed_at' => now(),
                ])->save();
            }

            if ($count === 0) {
                return;
            }

            $firstAt = $valid[0]['at'];
            $lastAt = $valid[$count - 1]['at'];
            $hasOut = $count >= 2;

            $attendance = Attendance::query()->firstOrCreate(
                [
                    'company_id' => $companyId,
                    'user_id' => $userId,
                    'date' => $attendanceDate,
                ],
                [
                    'status' => 'present',
                ]
            );

            if (! $attendance->manual_time_lock) {
                $isLate = $this->calendarDayResolver->isLateArrivalForUser($user, $firstAt);
                $payload = [
                    'clock_in_date_time' => $firstAt->format('Y-m-d H:i:s'),
                    'clock_in_time' => $firstAt->format('H:i:s'),
                    'clock_in_method' => 'biometric',
                    'attendance_type' => $attendance->attendance_type ?: 'office',
                    'status' => 'present',
                    'is_late' => $isLate,
                ];

                if ($hasOut) {
                    $duration = max(0, $firstAt->diffInSeconds($lastAt));
                    $payload['clock_out_date_time'] = $lastAt->format('Y-m-d H:i:s');
                    $payload['clock_out_time'] = $lastAt->format('H:i:s');
                    $payload['clock_out_method'] = 'biometric';
                    $payload['total_duration'] = $duration;
                } else {
                    $payload['clock_out_date_time'] = null;
                    $payload['clock_out_time'] = null;
                    $payload['clock_out_method'] = null;
                    $payload['total_duration'] = null;
                }

                $firstPunch = $valid[0]['punch'];
                if ($firstPunch->latitude || $firstPunch->longitude || $firstPunch->gps_location) {
                    $payload['clock_in_latitude'] = $firstPunch->latitude;
                    $payload['clock_in_longitude'] = $firstPunch->longitude;
                    $payload['clock_in_location'] = $firstPunch->gps_location;
                }
                if ($hasOut) {
                    $lastPunch = $valid[$count - 1]['punch'];
                    $payload['clock_out_latitude'] = $lastPunch->latitude;
                    $payload['clock_out_longitude'] = $lastPunch->longitude;
                    $payload['clock_out_location'] = $lastPunch->gps_location;
                }

                $attendance->fill($payload)->save();
            }

            $workDurationId = $this->syncFiloWorkDuration(
                $attendance,
                $companyId,
                $firstAt,
                $hasOut ? $lastAt : null,
                $tz
            );

            foreach ($valid as $row) {
                $row['punch']->forceFill([
                    'attendance_id' => $attendance->id,
                    'work_duration_id' => $workDurationId,
                ])->save();
            }

            foreach ($punches->where('is_duplicate', true) as $dup) {
                $dup->forceFill([
                    'attendance_id' => $attendance->id,
                    'work_duration_id' => $workDurationId,
                ])->save();
            }
        });
    }

    protected function syncFiloWorkDuration(
        Attendance $attendance,
        int $companyId,
        Carbon $firstAt,
        ?Carbon $lastAt,
        string $tz
    ): ?int {
        $noteKey = 'zkteco-filo:'.$attendance->id;

        // One FILO session per day. Drop legacy toggle pairs (zkteco:{punchId}) and extra filo rows.
        WorkDuration::query()
            ->where('company_id', $companyId)
            ->where('attendance_id', $attendance->id)
            ->where(function ($q) {
                $q->where('notes', 'like', 'zkteco:%')
                    ->orWhere('notes', 'like', 'zkteco-filo:%');
            })
            ->delete();

        $session = new WorkDuration([
            'company_id' => $companyId,
            'attendance_id' => $attendance->id,
            'start_time' => $firstAt->format('H:i:s'),
            'end_time' => $lastAt?->format('H:i:s'),
            'duration' => $lastAt ? max(0, $firstAt->diffInSeconds($lastAt)) : null,
            'status' => $lastAt ? 'closed' : 'open',
            'notes' => $noteKey,
        ]);
        $session->save();

        return (int) $session->id;
    }

    protected function minPunchGapMinutes(int $companyId): int
    {
        $policy = $this->policyResolver->forCompany($companyId);
        $gap = (int) ($policy->biometric_min_punch_gap_minutes
            ?? config('zkteco.min_punch_gap_minutes', 1));

        return max(1, $gap);
    }

    protected function resolveEmployee(Company $company, string $empCode): ?User
    {
        $empCode = trim($empCode);
        if ($empCode === '') {
            return null;
        }

        $base = User::query()
            ->where('company_id', $company->id)
            ->where('user_type', User::USER_TYPE_STAFF)
            ->where('status', User::STATUS_ACTIVE);

        $exact = (clone $base)->where('employee_number', $empCode)->first();
        if ($exact) {
            return $exact;
        }

        if (! ctype_digit($empCode)) {
            return null;
        }

        $normalizedCode = ltrim($empCode, '0');
        if ($normalizedCode === '') {
            $normalizedCode = '0';
        }

        $candidates = (clone $base)
            ->whereNotNull('employee_number')
            ->where('employee_number', '!=', '')
            ->get(['id', 'employee_number']);

        $matches = $candidates->filter(function (User $user) use ($normalizedCode) {
            $number = trim((string) $user->employee_number);
            if ($number === '' || ! ctype_digit($number)) {
                return false;
            }
            $normalizedNumber = ltrim($number, '0');
            if ($normalizedNumber === '') {
                $normalizedNumber = '0';
            }

            return $normalizedNumber === $normalizedCode;
        });

        if ($matches->count() === 1) {
            return User::query()->find($matches->first()->id);
        }

        return null;
    }

    protected function markError(AttendancePunch $punch, string $message): AttendancePunch
    {
        $punch->forceFill([
            'status' => AttendancePunch::STATUS_ERROR,
            'error_message' => $message,
            'processed_type' => null,
            'is_duplicate' => false,
            'processed_at' => now(),
        ])->save();

        return $punch->fresh() ?? $punch;
    }
}
