<?php

namespace App\Services;

use App\Enums\BookingType;
use App\Enums\RequestScope;
use App\Enums\TripStatus;
use App\Events\TripStateChanged;
use App\Models\Customer;
use App\Models\Driver;
use App\Models\FareQuote;
use App\Models\Trip;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class TripService
{
    public function __construct(private readonly FareService $fares, private readonly TripStatusService $statuses) {}

    public function quote(Customer $customer, array $data): FareQuote
    {
        return $this->fares->quote($customer, $data);
    }

    public function create(Customer $customer, array $data, User $actor): Trip
    {
        return DB::transaction(function () use ($customer, $data, $actor): Trip {
            $quote = FareQuote::where('public_id', $data['fare_quote_id'])->where('customer_id', $customer->id)->lockForUpdate()->firstOrFail();
            if ($quote->expires_at->isPast()) {
                throw new RuntimeException('Fare quote has expired.');
            }
            $scheduled = ($data['booking_type'] ?? BookingType::Immediate->value) === BookingType::Scheduled->value;
            $status = $scheduled ? TripStatus::Scheduled : TripStatus::Searching;
            $trip = Trip::create(['customer_id' => $customer->id, 'fare_quote_id' => $quote->id, 'booking_source' => 'customer_app', 'booking_type' => $scheduled ? BookingType::Scheduled : BookingType::Immediate, 'request_scope' => $data['request_scope'] ?? RequestScope::Global, 'driver_group_id' => $data['driver_group_id'] ?? null, 'requested_driver_id' => $data['requested_driver_id'] ?? null, 'pickup_latitude' => $data['pickup_latitude'], 'pickup_longitude' => $data['pickup_longitude'], 'pickup_address' => $data['pickup_address'] ?? null, 'destination_latitude' => $data['destination_latitude'], 'destination_longitude' => $data['destination_longitude'], 'destination_address' => $data['destination_address'] ?? null, 'scheduled_at' => $scheduled ? $data['scheduled_at'] : null, 'timezone' => $data['timezone'] ?? 'UTC', 'status' => $status, 'requested_at' => Carbon::now(), 'searching_at' => $scheduled ? null : Carbon::now()]);
            $this->statuses->log($trip, null, $status, $actor);
            TripStateChanged::dispatch($trip->fresh());

            return $trip->fresh(['fareQuote', 'customer.user']);
        });
    }

    public function accept(Trip $trip, Driver $driver, User $actor): Trip
    {
        return DB::transaction(function () use ($trip, $driver, $actor): Trip {
            $locked = Trip::lockForUpdate()->findOrFail($trip->id);
            $lockedDriver = Driver::query()->lockForUpdate()->findOrFail($driver->id);
            if ($locked->status !== TripStatus::Searching) {
                throw new RuntimeException('Trip is no longer available.');
            }
            if ($lockedDriver->status->value !== 'active' || ! $lockedDriver->is_online) {
                throw new RuntimeException('Driver is not eligible.');
            }
            if ($lockedDriver->trips()->whereIn('status', [TripStatus::Assigned, TripStatus::DriverEnRoute, TripStatus::DriverArrived, TripStatus::TripStarted])->exists()) {
                throw new RuntimeException('Finish the current trip before accepting another request.');
            }
            if (\App\Models\DriverGroupRouteRun::where('driver_id', $lockedDriver->id)->where('status', 'in_progress')->exists()) {
                throw new RuntimeException('Complete your current group route before accepting another request.');
            }
            if ($locked->request_scope === RequestScope::SpecificDriver && $locked->requested_driver_id !== $lockedDriver->id) {
                throw new RuntimeException('Trip is directed to another driver.');
            }
            if ($locked->request_scope === RequestScope::Group && ! $lockedDriver->groups()->whereKey($locked->driver_group_id)->wherePivot('status', 'approved')->exists()) {
                throw new RuntimeException('Driver is not a member of this group.');
            }
            $locked->update(['driver_id' => $lockedDriver->id, 'status' => TripStatus::Assigned, 'assigned_at' => Carbon::now()]);
            $this->statuses->log($locked, TripStatus::Searching, TripStatus::Assigned, $actor);
            TripStateChanged::dispatch($locked->fresh());

            return $locked->fresh(['fareQuote', 'customer.user', 'driver.user', 'vehicle']);
        });
    }

    public function available(Driver $driver)
    {
        if ($driver->current_latitude === null || $driver->current_longitude === null) {
            return new LengthAwarePaginator(collect(), 0, 20, 1, ['path' => Paginator::resolveCurrentPath()]);
        }
        $latitude = (float) $driver->current_latitude;
        $longitude = (float) $driver->current_longitude;
        $latDelta = 2 / 110.574;
        $lonDelta = 2 / max(111.320 * cos(deg2rad($latitude)), 1);

        $trips = Trip::query()->with(['fareQuote', 'customer.user'])->where('status', TripStatus::Searching)
            ->whereBetween('pickup_latitude', [$latitude - $latDelta, $latitude + $latDelta])
            ->whereBetween('pickup_longitude', [$longitude - $lonDelta, $longitude + $lonDelta])
            ->where(function ($query) use ($driver) {
                $query->where('request_scope', RequestScope::Global)
                    ->orWhere(function ($query) use ($driver) {
                        $query->where('request_scope', RequestScope::SpecificDriver)->where('requested_driver_id', $driver->id);
                    })
                    ->orWhere(function ($query) use ($driver) {
                        $query->where('request_scope', RequestScope::Group)->whereIn('driver_group_id', $driver->groups()->wherePivot('status', 'approved')->pluck('driver_groups.id'));
                    });
            })->latest('requested_at')->get()
            ->filter(fn (Trip $trip) => $this->distanceMeters($latitude, $longitude, (float) $trip->pickup_latitude, (float) $trip->pickup_longitude) <= 2000)
            ->values();

        $page = max((int) request()->query('page', 1), 1);
        return new LengthAwarePaginator($trips->forPage($page, 20)->values(), $trips->count(), 20, $page, ['path' => Paginator::resolveCurrentPath()]);
    }

    private function distanceMeters(float $originLatitude, float $originLongitude, float $destinationLatitude, float $destinationLongitude): float
    {
        $lat = deg2rad($destinationLatitude - $originLatitude);
        $lon = deg2rad($destinationLongitude - $originLongitude);
        $a = sin($lat / 2) ** 2 + cos(deg2rad($originLatitude)) * cos(deg2rad($destinationLatitude)) * sin($lon / 2) ** 2;

        return 6371000 * 2 * asin(min(1, sqrt($a)));
    }

    public function reject(Trip $trip, Driver $driver, User $actor): void
    {
        if ($trip->status !== TripStatus::Searching) {
            throw new RuntimeException('Trip is no longer available.');
        }
        $trip->statusLogs()->create(['event_type' => 'driver_rejected', 'previous_status' => $trip->status->value, 'new_status' => $trip->status->value, 'actor_user_id' => $actor->id, 'source' => 'api', 'metadata' => ['driver_id' => $driver->public_id]]);
    }

    public function activateScheduled(): int
    {
        $cutoff = now()->addMinutes((int) config('taxi.trip.scheduled_dispatch_lead_minutes'));

        return Trip::where('status', TripStatus::Scheduled)->whereNotNull('scheduled_at')->where('scheduled_at', '<=', $cutoff)->update(['status' => TripStatus::Searching, 'searching_at' => now()]);
    }

    public function transition(Trip $trip, TripStatus $expected, TripStatus $target, User $actor): Trip
    {
        return DB::transaction(function () use ($trip, $expected, $target, $actor): Trip {
            $locked = Trip::lockForUpdate()->findOrFail($trip->id);
            if ($locked->status !== $expected) {
                throw new RuntimeException('Trip is not in the expected state.');
            } $time = Carbon::now();
            $attrs = ['status' => $target];
            if ($target === TripStatus::DriverEnRoute) {
                $attrs['assigned_at'] = $time;
            } if ($target === TripStatus::TripStarted) {
                $attrs['started_at'] = $time;
            } if ($target === TripStatus::TripCompleted) {
                $attrs['completed_at'] = $time;
            } $locked->update($attrs);
            $this->statuses->log($locked, $expected, $target, $actor);
            TripStateChanged::dispatch($locked->fresh());

            return $locked->fresh(['fareQuote', 'customer.user', 'driver.user', 'vehicle']);
        });
    }

    public function cancel(Trip $trip, User $actor, ?string $reason): Trip
    {
        return DB::transaction(function () use ($trip, $actor, $reason): Trip {
            $locked = Trip::lockForUpdate()->findOrFail($trip->id);
            $allowed = [TripStatus::Searching, TripStatus::Assigned, TripStatus::DriverEnRoute, TripStatus::DriverArrived, TripStatus::Scheduled];
            if (! in_array($locked->status, $allowed, true)) {
                throw new RuntimeException('Trip cannot be cancelled now.');
            } $from = $locked->status;
            $locked->update(['status' => TripStatus::Cancelled, 'cancelled_at' => Carbon::now(), 'cancelled_by_user_id' => $actor->id, 'cancellation_reason' => $reason]);
            $this->statuses->log($locked, $from, TripStatus::Cancelled, $actor, 'api', ['reason' => $reason]);
            TripStateChanged::dispatch($locked->fresh());

            return $locked->fresh(['fareQuote', 'customer.user', 'driver.user']);
        });
    }
}
