<?php

namespace App\Services;

use App\Models\AttendanceSetting;
use App\Models\Employee;
use App\Models\OfficeLocation;
use Carbon\Carbon;

class AttendanceService
{
    /**
     * Normalize coordinate string (convert comma to dot, trim whitespace).
     */
    public static function normalizeCoordinate(mixed $coord): float
    {
        if (is_numeric($coord)) {
            return (float)$coord;
        }
        $cleaned = str_replace([' ', ','], ['', '.'], (string)$coord);
        return is_numeric($cleaned) ? (float)$cleaned : 0.0;
    }

    /**
     * Calculate distance between two GPS coordinates using Haversine formula.
     * Returns distance in meters with high precision.
     */
    public static function calculateDistance(float|string $lat1, float|string $lon1, float|string $lat2, float|string $lon2): float
    {
        $lat1 = static::normalizeCoordinate($lat1);
        $lon1 = static::normalizeCoordinate($lon1);
        $lat2 = static::normalizeCoordinate($lat2);
        $lon2 = static::normalizeCoordinate($lon2);

        $earthRadius = 6371000; // in meters

        $latDelta = deg2rad($lat2 - $lat1);
        $lonDelta = deg2rad($lon2 - $lon1);

        $a = sin($latDelta / 2) * sin($latDelta / 2) +
            cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
            sin($lonDelta / 2) * sin($lonDelta / 2);

        // Clamp to valid range [0, 1] to prevent NaN on floating point edge cases
        $a = min(1.0, max(0.0, $a));

        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));

        return round($earthRadius * $c, 2);
    }

    /**
     * Find nearest office location from employee GPS coordinate.
     */
    public static function findNearestOffice(float $latitude, float $longitude): ?array
    {
        $offices = OfficeLocation::where('is_active', true)->get();

        if ($offices->isEmpty()) {
            return null;
        }

        $nearestOffice = null;
        $minDistance = INF;

        foreach ($offices as $office) {
            $dist = static::calculateDistance($latitude, $longitude, (float)$office->latitude, (float)$office->longitude);
            if ($dist < $minDistance) {
                $minDistance = $dist;
                $nearestOffice = $office;
            }
        }

        return [
            'office' => $nearestOffice,
            'distance' => $minDistance,
            'is_within_radius' => $nearestOffice ? ($minDistance <= $nearestOffice->radius_meters) : false,
        ];
    }

    /**
     * Validate GPS Location, Server-side Haversine Distance, and GPS Accuracy.
     * Evaluates strictly against Admin Dashboard Radius and Max GPS Accuracy.
     */
    public static function validateLocationAndAccuracy(
        float $latitude,
        float $longitude,
        ?float $accuracy = null,
        ?int $officeId = null
    ): array {
        // 1. Resolve Office
        $office = null;
        if ($officeId) {
            $office = OfficeLocation::where('is_active', true)->find($officeId);
        }

        if (!$office) {
            $nearest = static::findNearestOffice($latitude, $longitude);
            $office = $nearest['office'] ?? null;
        }

        if (!$office) {
            return [
                'valid' => false,
                'error_type' => 'no_office',
                'message' => 'Tidak ada lokasi kantor aktif yang terdaftar dalam sistem.',
                'distance' => 0,
                'accuracy' => $accuracy,
                'office' => null,
            ];
        }

        // 2. Validate Maximum GPS Accuracy
        $maxGpsAccuracy = $office->max_gps_accuracy ?? (int)AttendanceSetting::get('max_gps_accuracy', 30);
        if ($accuracy !== null && $accuracy > $maxGpsAccuracy) {
            return [
                'valid' => false,
                'error_type' => 'accuracy',
                'message' => "Akurasi lokasi terlalu rendah (" . round($accuracy, 1) . " meter > batas {$maxGpsAccuracy} meter). Silakan aktifkan GPS dengan akurasi tinggi dan coba lagi.",
                'distance' => 0,
                'accuracy' => $accuracy,
                'max_accuracy' => $maxGpsAccuracy,
                'office' => $office,
            ];
        }

        // 3. Calculate Strict Server-Side Haversine Distance
        $distance = static::calculateDistance($latitude, $longitude, (float)$office->latitude, (float)$office->longitude);
        $radius = (float)$office->radius_meters;

        if ($distance > $radius) {
            return [
                'valid' => false,
                'error_type' => 'radius',
                'message' => "Presensi ditolak. Anda berada di luar radius lokasi ({$office->name}). Jarak Anda: " . round($distance, 1) . " meter (Batas radius: {$radius} meter).",
                'distance' => $distance,
                'accuracy' => $accuracy,
                'radius' => $radius,
                'office' => $office,
            ];
        }

        return [
            'valid' => true,
            'error_type' => null,
            'message' => "Lokasi terverifikasi valid. Jarak: " . round($distance, 1) . "m dari {$office->name} (Radius: {$radius}m, Akurasi: " . ($accuracy ? round($accuracy, 1) . "m" : "-") . ").",
            'distance' => $distance,
            'accuracy' => $accuracy,
            'radius' => $radius,
            'office' => $office,
        ];
    }

    /**
     * Determine attendance status and late minutes based on shift and check-in time.
     */
    public static function evaluateCheckInStatus(Employee $employee, Carbon $checkInTime): array
    {
        $schedule = $employee->currentSchedule();
        $shift = $schedule?->shift;

        if (!$shift) {
            return [
                'status' => 'present',
                'late_minutes' => 0,
                'shift_id' => null,
            ];
        }

        $shiftStartTime = Carbon::parse($checkInTime->toDateString() . ' ' . $shift->start_time);
        $toleranceMinutes = $shift->late_tolerance_minutes ?? (int)AttendanceSetting::get('late_tolerance_minutes', 10);
        $deadlineTime = $shiftStartTime->copy()->addMinutes($toleranceMinutes);

        if ($checkInTime->gt($deadlineTime)) {
            $lateMinutes = (int)$shiftStartTime->diffInMinutes($checkInTime, false);
            return [
                'status' => 'late',
                'late_minutes' => max(1, $lateMinutes),
                'shift_id' => $shift->id,
            ];
        }

        return [
            'status' => 'present',
            'late_minutes' => 0,
            'shift_id' => $shift->id,
        ];
    }
}
