<?php

namespace App\Services;

use App\Models\{Order, DeliveryBoy, DeliveryAssignment, DeliveryRejection};
use Illuminate\Support\Facades\{DB, Log};
use Carbon\Carbon;

class DeliveryMatchingService
{
    private const MAX_RADIUS_KM = 5;
    private const TOP_CANDIDATES = 3;
    
    private const WEIGHTS = [
        'distance' => 0.40,
        'rating' => 0.25,
        'acceptance' => 0.25,
        'activity' => 0.10,
    ];

    public function findAndAssignDeliveryBoy(Order $order): ?DeliveryBoy
    {
        return DB::transaction(function () use ($order) {
            $order = Order::lockForUpdate()->find($order->id);
            
            if ($order->assigned_delivery_boy_id) {
                return DeliveryBoy::find($order->assigned_delivery_boy_id);
            }

            $candidates = $this->findCandidates($order);
            
            if ($candidates->isEmpty()) {
                Log::warning("No delivery boys found for order {$order->id}");
                return null;
            }

            $rankedCandidates = $this->rankCandidates($candidates);
            
            return $this->createAssignments($order, $rankedCandidates);
        });
    }

    private function findCandidates(Order $order)
    {
        $restaurant = $order->restaurant;
        
        return DeliveryBoy::select('delivery_boys.*')
            ->selectRaw('
                (6371 * acos(
                    cos(radians(?)) * cos(radians(delivery_boy_areas.latitude)) *
                    cos(radians(delivery_boy_areas.longitude) - radians(?)) +
                    sin(radians(?)) * sin(radians(delivery_boy_areas.latitude))
                )) AS distance_km',
                [$restaurant->latitude, $restaurant->longitude, $restaurant->latitude]
            )
            ->join('delivery_boy_areas', 'delivery_boys.id', '=', 'delivery_boy_areas.delivery_boy_id')
            ->where('delivery_boys.status', 'online')
            ->where('delivery_boys.is_available', true)
            ->where('delivery_boys.verification_status', 'verified')
            ->where('delivery_boy_areas.is_active', true)
            ->havingRaw('distance_km <= ?', [self::MAX_RADIUS_KM])
            ->orderBy('distance_km')
            ->limit(20)
            ->get();
    }

    private function rankCandidates($candidates)
    {
        return $candidates->map(function ($boy) {
            $distanceScore = (1 - ($boy->distance_km / self::MAX_RADIUS_KM)) * 100;
            $ratingScore = ($boy->rating / 5) * 100;
            $acceptanceScore = $boy->total_deliveries > 0 
                ? ($boy->successful_deliveries / $boy->total_deliveries) * 100 
                : 50;
            
            $minutesSinceActive = $boy->last_active_at 
                ? Carbon::parse($boy->last_active_at)->diffInMinutes(now())
                : 60;
            $activityScore = max(0, (1 - ($minutesSinceActive / 60)) * 100);
            
            $boy->score = 
                (self::WEIGHTS['distance'] * $distanceScore) +
                (self::WEIGHTS['rating'] * $ratingScore) +
                (self::WEIGHTS['acceptance'] * $acceptanceScore) +
                (self::WEIGHTS['activity'] * $activityScore);
            
            return $boy;
        })
        ->sortByDesc('score')
        ->take(self::TOP_CANDIDATES)
        ->values();
    }

    private function createAssignments(Order $order, $candidates): ?DeliveryBoy
    {
        foreach ($candidates as $index => $candidate) {
            DeliveryAssignment::create([
                'order_id' => $order->id,
                'delivery_boy_id' => $candidate->id,
                'distance_km' => $candidate->distance_km,
                'score' => $candidate->score,
                'rank' => $index + 1,
                'status' => 'pending',
                'notified_at' => now(),
            ]);
        }
        
        $order->update([
            'assignment_attempted_at' => now(),
            'assignment_attempts' => $order->assignment_attempts + 1,
        ]);
        
        return $candidates->first();
    }

    public function acceptAssignment(DeliveryAssignment $assignment): bool
    {
        return DB::transaction(function () use ($assignment) {
            $assignment = DeliveryAssignment::lockForUpdate()->find($assignment->id);
            $order = Order::lockForUpdate()->find($assignment->order_id);
            $deliveryBoy = DeliveryBoy::lockForUpdate()->find($assignment->delivery_boy_id);
            
            if ($assignment->status !== 'pending' || $order->assigned_delivery_boy_id) {
                return false;
            }
            
            if (!$deliveryBoy->is_available || $deliveryBoy->status !== 'online') {
                $assignment->update(['status' => 'expired']);
                return false;
            }
            
            $assignment->update([
                'status' => 'accepted',
                'responded_at' => now(),
            ]);
            
            \App\Models\Delivery::create([
                'order_id' => $order->id,
                'delivery_boy_id' => $deliveryBoy->id,
                'status' => 'assigned',
                'assigned_at' => now(),
                'pickup_address' => $order->restaurant->address ?? 'Restaurant',
                'pickup_latitude' => $order->restaurant->latitude ?? 0,
                'pickup_longitude' => $order->restaurant->longitude ?? 0,
                'delivery_address' => $order->delivery_address ?? 'Customer',
                'delivery_latitude' => $order->delivery_latitude ?? 0,
                'delivery_longitude' => $order->delivery_longitude ?? 0,
                'distance_km' => $order->distance_km ?? 0,
                'delivery_fee' => $order->delivery_fee ?? 0,
            ]);
            
            $order->update([
                'assigned_delivery_boy_id' => $deliveryBoy->id,
                'status' => 'picked_up',
            ]);
            
            $deliveryBoy->update([
                'status' => 'on_delivery',
                'is_available' => false,
            ]);
            
            DeliveryAssignment::where('order_id', $order->id)
                ->where('id', '!=', $assignment->id)
                ->where('status', 'pending')
                ->update(['status' => 'expired']);
            
            return true;
        });
    }

    public function rejectAssignment(DeliveryAssignment $assignment, string $reason): void
    {
        DB::transaction(function () use ($assignment, $reason) {
            $assignment->update([
                'status' => 'rejected',
                'responded_at' => now(),
            ]);
            
            DeliveryRejection::create([
                'order_id' => $assignment->order_id,
                'delivery_boy_id' => $assignment->delivery_boy_id,
                'reason' => $reason,
            ]);
        });
    }
}
