<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\BookingDispatchRequest;
use App\Http\Requests\Admin\BookingFullUpdateRequest;
use App\Http\Requests\Admin\BookingRequest;
use App\Jobs\ExpireBookingAssignment;
use App\Models\Account;
use App\Models\Booking;
use App\Models\BookingStatus;
use App\Models\Customer;
use App\Models\Driver;
use App\Models\DriverShift;
use App\Models\PaymentMethod;
use App\Models\VehicleType;
use App\Services\BookingAssignmentService;
use App\Services\FareCalculator;
use App\Services\FcmService;
use App\Services\GoogleMapsService;
use App\Services\ReceiptService;
use App\Services\SmsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;

class BookingController extends Controller
{
    public function index(Request $request): View
    {
        $companyId = $request->user()->company_id;

        // Unlike drivers/vehicles/customers (naturally bounded by fleet
        // size), a company's booking history keeps growing indefinitely —
        // the datatable below still gets client-side search/sort/paginate,
        // just over the most recent 500 rather than the true entire table.
        $bookings = Booking::with(['customer', 'driver', 'status', 'vehicleType'])
            ->when($companyId, fn ($q) => $q->where('company_id', $companyId))
            ->when($request->filled('status_id'), fn ($q) => $q->where('status_id', $request->input('status_id')))
            ->latest('pickup_datetime')
            ->limit(500)
            ->get();

        return view('admin.bookings.index', [
            'bookings' => $bookings,
            'statuses' => BookingStatus::orderBy('sort_order')->get(),
        ]);
    }

    public function create(Request $request): View
    {
        $companyId = $request->user()->company_id;
        $scope = fn ($q) => $q->when($companyId, fn ($q2) => $q2->where('company_id', $companyId));

        $todayBookings = Booking::when($companyId, fn ($q) => $q->where('company_id', $companyId))->whereDate('pickup_datetime', today());

        $jobStats = [
            'total' => (clone $todayBookings)->count(),
            'new' => (clone $todayBookings)->whereHas('status', fn ($q) => $q->where('name', 'Unassigned'))->count(),
            'future' => (clone $todayBookings)->whereHas('status', fn ($q) => $q->where('name', 'Pending'))->count(),
            'completed' => (clone $todayBookings)->whereHas('status', fn ($q) => $q->where('name', 'Completed'))->count(),
        ];

        $drivers = $scope(Driver::with('vehicles:id,driver_id,vehicle_type_id'))->where('status', 'active')->orderBy('first_name')->get();

        return view('admin.bookings.create', [
            'booking' => new Booking(['passengers' => 1, 'luggage' => 0, 'pickup_datetime' => now()->format('Y-m-d\TH:i')]),
            'customers' => $scope(Customer::query())->orderBy('name')->get(),
            'vehicleTypes' => $scope(VehicleType::with('currency'))->where('is_active', true)->get(),
            'paymentMethods' => PaymentMethod::where('is_active', true)->orderBy('name')->get(),
            'drivers' => $drivers,
            // Which vehicle type(s) each driver actually drives — lets the
            // dispatch console narrow "Assign Driver" down to drivers who
            // have the vehicle being booked, once one is known.
            'driverVehicleTypes' => $drivers->mapWithKeys(fn (Driver $driver) => [
                $driver->id => $driver->vehicles->pluck('vehicle_type_id')->unique()->values(),
            ]),
            'accounts' => $scope(Account::query())->where('is_active', true)->orderBy('name')->get(),
            'jobStats' => $jobStats,
            'recentBookings' => Booking::with(['customer', 'driver', 'status'])
                ->when($companyId, fn ($q) => $q->where('company_id', $companyId))
                ->latest('pickup_datetime')->take(10)->get(),
            'statuses' => BookingStatus::orderBy('sort_order')->get(),
        ]);
    }

    public function quote(Request $request, GoogleMapsService $maps, FareCalculator $fares): JsonResponse
    {
        $data = $request->validate([
            'pickup_address' => ['required', 'string'],
            'dropoff_address' => ['required', 'string'],
            'vehicle_type_id' => ['required', 'exists:vehicle_types,id'],
        ]);

        $vehicleType = VehicleType::findOrFail($data['vehicle_type_id']);
        $company = $request->user()->company ?? $vehicleType->company;
        $unit = $company->distance_unit ?? 'mi';

        try {
            $trip = $maps->distanceAndDuration($data['pickup_address'], $data['dropoff_address'], $unit);
        } catch (\Throwable $e) {
            return response()->json(['message' => $e->getMessage()], 422);
        }

        $fare = $fares->calculate($vehicleType, $trip['distance']);

        return response()->json([
            'distance' => $trip['distance'],
            'distance_unit' => $unit,
            'duration_minutes' => $trip['duration_minutes'],
            'fare_amount' => $fare,
            'currency_symbol' => $vehicleType->currency->symbol ?? '',
        ]);
    }

    /**
     * Quotes every active vehicle type for one route in a single Distance
     * Matrix call — powers the dispatch console's vehicle-type cards
     * (mirrors Api\Customer\FareController::quote for the same reason:
     * one route, many cars, one Google call instead of one per car).
     */
    public function quoteAll(Request $request, GoogleMapsService $maps, FareCalculator $fares): JsonResponse
    {
        $data = $request->validate([
            'pickup_address' => ['required', 'string'],
            'dropoff_address' => ['required', 'string'],
        ]);

        $companyId = $request->user()->company_id;
        $unit = $request->user()->company->distance_unit ?? 'mi';

        try {
            $trip = $maps->distanceAndDuration($data['pickup_address'], $data['dropoff_address'], $unit);
        } catch (\Throwable $e) {
            return response()->json(['message' => $e->getMessage()], 422);
        }

        $vehicleTypes = VehicleType::with('currency')
            ->when($companyId, fn ($q) => $q->where('company_id', $companyId))
            ->where('is_active', true)
            ->get();

        $quotes = $vehicleTypes->map(fn (VehicleType $type) => [
            'vehicle_type_id' => $type->id,
            'name' => $type->name,
            'capacity' => $type->capacity,
            'image_path' => $type->image_path,
            'per_distance_rate' => $type->per_distance_rate,
            'fare_amount' => $fares->calculate($type, $trip['distance']),
            'currency_symbol' => $type->currency->symbol ?? '',
        ]);

        return response()->json([
            'distance' => $trip['distance'],
            'distance_unit' => $unit,
            'duration_minutes' => $trip['duration_minutes'],
            'vehicle_types' => $quotes,
        ]);
    }

    /**
     * Live on-shift driver positions for the dispatch console's map —
     * polled rather than pushed over the DriverLocationUpdated broadcast,
     * so the map still works even when no real Pusher app is configured
     * (BROADCAST_CONNECTION defaults to "log" until one is set up).
     */
    public function driverLocations(Request $request): JsonResponse
    {
        $companyId = $request->user()->company_id;

        $shifts = DriverShift::with('driver:id,first_name,last_name')
            ->whereNull('ended_at')
            ->whereNotNull('last_lat')
            ->whereNotNull('last_lng')
            ->whereHas('driver', fn ($q) => $q->when($companyId, fn ($q2) => $q2->where('company_id', $companyId)))
            ->get();

        return response()->json([
            'drivers' => $shifts->map(fn ($shift) => [
                'driver_id' => $shift->driver_id,
                'name' => $shift->driver->full_name ?? 'Driver',
                'lat' => $shift->last_lat,
                'lng' => $shift->last_lng,
                'is_available' => $shift->is_available,
            ]),
        ]);
    }

    /**
     * Pre-assign check for the dispatch console — wraps the same
     * blockingReason() gate store()/update() enforce so the UI can warn
     * ("driver has an active job — assign anyway?") before the user commits.
     */
    public function checkDriver(Request $request, BookingAssignmentService $assignment): JsonResponse
    {
        $data = $request->validate([
            'driver_id' => ['required', 'exists:drivers,id'],
            'booking_id' => ['nullable', 'exists:bookings,id'],
        ]);

        $driver = Driver::findOrFail($data['driver_id']);
        $this->authorizeCompanyOwnership($driver);

        $reason = $assignment->blockingReason($driver, $data['booking_id'] ?? null);

        return response()->json(['blocked' => (bool) $reason, 'message' => $reason]);
    }

    /**
     * BK-01: assign or cancel a batch of jobs in one request from the jobs
     * table's row checkboxes. Each booking is still validated/assigned one at
     * a time (reusing the same blockingReason() rule update() applies), so a
     * driver already at their queue cap simply stops absorbing further rows
     * from the batch rather than the whole request failing.
     */
    public function bulkAction(Request $request, FcmService $fcm, BookingAssignmentService $assignment): JsonResponse
    {
        $data = $request->validate([
            'action' => ['required', 'in:assign,cancel'],
            'booking_ids' => ['required', 'array', 'min:1'],
            'booking_ids.*' => ['integer', 'exists:bookings,id'],
            'driver_id' => ['required_if:action,assign', 'nullable', 'exists:drivers,id'],
            'reason' => ['nullable', 'string', 'max:500'],
        ]);

        $bookings = Booking::whereIn('id', $data['booking_ids'])->with('status', 'driver.user', 'customer')->get();
        foreach ($bookings as $booking) {
            $this->authorizeCompanyOwnership($booking);
        }

        $driver = null;
        if ($data['action'] === 'assign') {
            $driver = Driver::findOrFail($data['driver_id']);
            $this->authorizeCompanyOwnership($driver);
        }

        $succeeded = [];
        $failed = [];
        $cancelledId = BookingStatus::where('name', 'Cancelled')->value('id');
        $pendingId = BookingStatus::where('name', 'Pending')->value('id');

        foreach ($bookings as $booking) {
            if ($data['action'] === 'assign') {
                if (in_array($booking->status->name, ['Completed', 'Cancelled'], true)) {
                    $failed[] = ['id' => $booking->id, 'reason' => "{$booking->booking_number} is already {$booking->status->name}"];

                    continue;
                }

                $driverChanged = $booking->driver_id != $driver->id;

                // Reassigning a job already held by another driver is a
                // "retrieve" — only allowed while that driver still hasn't
                // started it (Pending/Accepted); once they've arrived or
                // gone onboard it's no longer up for grabs.
                if ($driverChanged && $booking->driver_id && ! in_array($booking->status->name, ['Pending', 'Accepted'], true)) {
                    $failed[] = ['id' => $booking->id, 'reason' => "{$booking->booking_number} can't be reassigned once it's {$booking->status->name}"];

                    continue;
                }

                if ($reason = $assignment->blockingReason($driver, $booking->id)) {
                    $failed[] = ['id' => $booking->id, 'reason' => "{$booking->booking_number}: {$reason}"];

                    continue;
                }

                $booking->update([
                    'driver_id' => $driver->id,
                    'status_id' => $driverChanged ? $pendingId : $booking->status_id,
                    'assigned_by' => $driverChanged ? $request->user()->id : $booking->assigned_by,
                    'assigned_at' => $driverChanged ? now() : $booking->assigned_at,
                ]);

                if ($driverChanged) {
                    $booking->statusHistory()->create([
                        'status_id' => $pendingId,
                        'note' => 'Bulk-assigned',
                        'changed_by' => $request->user()->id,
                    ]);

                    ExpireBookingAssignment::dispatch($booking->id)->delay(now()->addSeconds(40));
                }

                if ($driverChanged && $driver->user) {
                    $fcm->sendToOwner($driver->user, 'New job assigned', 'Booking '.$booking->booking_number.' has been assigned to you.',
                        $this->driverJobPayload($booking->fresh(), 'booking_assigned'));
                }

                $succeeded[] = $booking->id;
            } else {
                if (in_array($booking->status->name, ['Completed', 'Cancelled'], true)) {
                    $failed[] = ['id' => $booking->id, 'reason' => "{$booking->booking_number} is already {$booking->status->name}"];

                    continue;
                }

                $oldDriverUser = $booking->driver?->user;

                $booking->update(['status_id' => $cancelledId]);
                $booking->statusHistory()->create([
                    'status_id' => $cancelledId,
                    'note' => 'Bulk-cancelled'.(($data['reason'] ?? null) ? ' — '.$data['reason'] : ''),
                    'changed_by' => $request->user()->id,
                ]);

                if ($oldDriverUser) {
                    $fcm->sendToOwner($oldDriverUser, 'Job cancelled', 'Booking '.$booking->booking_number.' has been cancelled.',
                        $this->driverJobPayload($booking->fresh(), 'booking_cancelled'));
                }

                if ($booking->customer) {
                    $fcm->sendToOwner($booking->customer, 'Booking update', 'Your booking '.$booking->booking_number.' is now Cancelled.', [
                        'type' => 'booking_status_changed', 'booking_id' => (string) $booking->id,
                    ]);
                }

                $succeeded[] = $booking->id;
            }
        }

        return response()->json(['succeeded' => $succeeded, 'failed' => $failed]);
    }

    /**
     * Today's jobs + stat tiles for the dispatch console's live table —
     * polled client-side instead of server-side DataTables (no such
     * dependency exists in this app).
     */
    public function live(Request $request): JsonResponse
    {
        $companyId = $request->user()->company_id;

        // Stat tiles always reflect *today*, regardless of the table's own
        // date-range/status/search filters below.
        $todayBookings = Booking::when($companyId, fn ($q) => $q->where('company_id', $companyId))->whereDate('pickup_datetime', today());

        // Future Jobs (minicab-style): pre-booked trips that haven't happened
        // yet, regardless of what day they fall on — distinct from the
        // "today" stat tiles above, which is why it's scoped off company
        // bookings directly rather than off $todayBookings.
        $futureJobsCount = Booking::when($companyId, fn ($q) => $q->where('company_id', $companyId))
            ->where('pickup_datetime', '>', now())
            ->whereDoesntHave('status', fn ($q) => $q->whereIn('name', ['Completed', 'Cancelled', 'No Show']))
            ->count();

        $jobStats = [
            'total' => (clone $todayBookings)->count(),
            'new' => (clone $todayBookings)->whereHas('status', fn ($q) => $q->where('name', 'Unassigned'))->count(),
            'future' => (clone $todayBookings)->whereHas('status', fn ($q) => $q->where('name', 'Pending'))->count(),
            'completed' => (clone $todayBookings)->whereHas('status', fn ($q) => $q->where('name', 'Completed'))->count(),
            'futureJobs' => $futureJobsCount,
        ];

        $isFutureView = $request->boolean('future');

        $startDate = $request->input('start_date') ?: today()->toDateString();
        $endDate = $request->input('end_date') ?: today()->toDateString();

        $bookings = Booking::when($companyId, fn ($q) => $q->where('company_id', $companyId))
            ->when($isFutureView, function ($q) {
                // "Future Jobs" ignores the date-range filter entirely — it's
                // asking "what's still ahead of us right now", which can
                // span today and any later day at once.
                $q->where('pickup_datetime', '>', now())
                    ->whereDoesntHave('status', fn ($q2) => $q2->whereIn('name', ['Completed', 'Cancelled', 'No Show']));
            }, function ($q) use ($startDate, $endDate) {
                $q->whereDate('pickup_datetime', '>=', $startDate)
                    ->whereDate('pickup_datetime', '<=', $endDate);
            })
            ->when($request->filled('status_id'), fn ($q) => $q->where('status_id', $request->input('status_id')))
            ->when($request->filled('search'), function ($q) use ($request) {
                $term = '%'.$request->input('search').'%';
                $q->where(function ($q2) use ($term) {
                    $q2->where('booking_number', 'like', $term)
                        ->orWhere('pickup_address', 'like', $term)
                        ->orWhere('dropoff_address', 'like', $term)
                        ->orWhereHas('customer', fn ($q3) => $q3->where('name', 'like', $term)->orWhere('phone', 'like', $term));
                });
            })
            ->with(['customer', 'driver', 'status', 'paymentMethod', 'vehicleType.currency'])
            // Future jobs read naturally soonest-first; every other view
            // (today, a date range, a status) reads newest-first as before.
            ->when($isFutureView, fn ($q) => $q->oldest('pickup_datetime'), fn ($q) => $q->latest('pickup_datetime'))
            ->get()
            ->map(fn (Booking $booking) => [
                'id' => $booking->id,
                'booking_number' => $booking->booking_number,
                'pickup_datetime' => $booking->pickup_datetime->format('d M, H:i'),
                'pickup_address' => $booking->pickup_address,
                'dropoff_address' => $booking->dropoff_address,
                'customer_name' => $booking->customer->name ?? '—',
                'customer_phone' => $booking->customer->phone ?? '',
                'driver_name' => $booking->driver->full_name ?? 'Unassigned',
                'driver_id' => $booking->driver_id,
                'status_id' => $booking->status_id,
                'status_name' => $booking->status->name,
                'payment_method_id' => $booking->payment_method_id,
                'payment_method_name' => $booking->paymentMethod->name ?? '—',
                'fare_amount' => $booking->fare_amount,
                'currency_symbol' => $booking->vehicleType->currency->symbol ?? '',
                'pickup_lat' => $booking->pickup_lat,
                'pickup_lng' => $booking->pickup_lng,
                'dropoff_lat' => $booking->dropoff_lat,
                'dropoff_lng' => $booking->dropoff_lng,
            ]);

        return response()->json(['bookings' => $bookings, 'jobStats' => $jobStats]);
    }

    /**
     * Trip details from a past booking, used to prefill the booking form for
     * a repeat trip. Deliberately excludes driver_id/pickup_datetime/status —
     * a rebook is a fresh, unassigned job for a new date/time, not a copy of
     * the old one's dispatch state.
     */
    public function rebookData(Request $request, Booking $booking): JsonResponse
    {
        $this->authorizeCompanyOwnership($booking);
        $booking->load('stops', 'customer');

        return response()->json([
            'customer_id' => $booking->customer_id,
            'pickup_address' => $booking->pickup_address,
            'pickup_lat' => $booking->pickup_lat,
            'pickup_lng' => $booking->pickup_lng,
            'dropoff_address' => $booking->dropoff_address,
            'dropoff_lat' => $booking->dropoff_lat,
            'dropoff_lng' => $booking->dropoff_lng,
            'stops' => $booking->stops->pluck('address'),
            'vehicle_type_id' => $booking->vehicle_type_id,
            'passengers' => $booking->passengers,
            'luggage' => $booking->luggage,
            'flight_number' => $booking->flight_number,
            'customer_reference' => $booking->customer_reference,
            'notes' => $booking->notes,
            'payment_method_id' => $booking->payment_method_id,
            'account_id' => $booking->account_id,
        ]);
    }

    /**
     * Status-change audit trail for a booking, read from the same
     * statusHistory relation the edit page already renders server-side.
     */
    public function history(Request $request, Booking $booking): JsonResponse
    {
        $this->authorizeCompanyOwnership($booking);

        $history = $booking->statusHistory()->with('status', 'changedBy')->get()->map(fn ($entry) => [
            'status_name' => $entry->status->name,
            'note' => $entry->note,
            'changed_by' => $entry->changedBy->name ?? 'System',
            'created_at' => $entry->created_at->format('d M Y, H:i'),
        ]);

        return response()->json(['history' => $history]);
    }

    /**
     * Creates one Booking row per vehicle unit requested (e.g. 2 Saloons + 1
     * Estate for the same trip = 3 rows), sharing a group_id when more than
     * one is created so the jobs table/edit page can link them together.
     * BookingRequest already rejects driver_id when the total quantity is
     * more than one — a single driver can't be dispatched to multiple cars.
     */
    public function store(BookingRequest $request, GoogleMapsService $maps, FareCalculator $fares, SmsService $sms, BookingAssignmentService $assignment, FcmService $fcm): RedirectResponse
    {
        $data = $request->validated();
        $company = $request->user()->company ?? VehicleType::findOrFail($data['vehicles'][0]['vehicle_type_id'])->company;
        $unit = $company->distance_unit ?? 'mi';

        try {
            $trip = $maps->distanceAndDuration($data['pickup_address'], $data['dropoff_address'], $unit);
        } catch (\Throwable $e) {
            return back()->withInput()->with('error', 'Could not calculate the route: '.$e->getMessage());
        }

        if (! empty($data['driver_id'])) {
            $driver = Driver::findOrFail($data['driver_id']);
            $this->authorizeCompanyOwnership($driver);

            if ($reason = $assignment->blockingReason($driver)) {
                return back()->withInput()->with('error', $reason);
            }
        }

        if (($data['account_id'] ?? null) === '__new__') {
            $data['account_id'] = Account::create([
                'company_id' => $company->id,
                'name' => $data['new_account_name'],
                'account_number' => $data['new_account_number'] ?? null,
            ])->id;
        }

        if ($data['customer_id'] === '__new__') {
            $data['customer_id'] = Customer::firstOrCreate(
                ['company_id' => $company->id, 'phone' => $data['quick_customer_phone']],
                ['name' => $data['quick_customer_name'], 'email' => $data['quick_customer_email'] ?? null, 'status' => 'active']
            )->id;
        }

        $stops = $data['stops'] ?? [];
        $sendSms = (bool) ($data['send_sms'] ?? false);
        $vehicles = $data['vehicles'];
        $totalQuantity = collect($vehicles)->sum('quantity');
        $statusId = BookingStatus::where('name', ! empty($data['driver_id']) ? 'Pending' : 'Unassigned')->value('id');
        $groupId = $totalQuantity > 1 ? (string) Str::uuid() : null;

        $shared = [
            'group_id' => $groupId,
            'company_id' => $company->id,
            'customer_id' => $data['customer_id'],
            'account_id' => $data['account_id'] ?? null,
            'payment_method_id' => $data['payment_method_id'] ?? null,
            'pickup_address' => $data['pickup_address'],
            'pickup_lat' => $data['pickup_lat'] ?? null,
            'pickup_lng' => $data['pickup_lng'] ?? null,
            'dropoff_address' => $data['dropoff_address'],
            'dropoff_lat' => $data['dropoff_lat'] ?? null,
            'dropoff_lng' => $data['dropoff_lng'] ?? null,
            'pickup_datetime' => $data['pickup_datetime'],
            'passengers' => $data['passengers'],
            'luggage' => $data['luggage'] ?? 0,
            'flight_number' => $data['flight_number'] ?? null,
            'customer_reference' => $data['customer_reference'] ?? null,
            'notes' => $data['notes'] ?? null,
            'status_id' => $statusId,
            'distance' => $trip['distance'],
            'distance_unit' => $unit,
            'duration_minutes' => $trip['duration_minutes'],
            'job_type' => 'controller',
        ];

        if (! empty($data['driver_id'])) {
            $shared['driver_id'] = $data['driver_id'];
            $shared['assigned_by'] = $request->user()->id;
            $shared['assigned_at'] = now();
        }

        $bookings = collect();
        $summaryParts = [];
        $fareTotal = 0.0;

        foreach ($vehicles as $line) {
            $vehicleType = VehicleType::findOrFail($line['vehicle_type_id']);
            $fareOverride = $line['fare_override'] ?? null;
            $fareEach = $fareOverride !== null && $fareOverride !== ''
                ? round((float) $fareOverride, 2)
                : $fares->calculate($vehicleType, $trip['distance']);

            $summaryParts[] = $line['quantity'].'× '.$vehicleType->name;

            for ($i = 0; $i < $line['quantity']; $i++) {
                $booking = Booking::create($shared + [
                    'vehicle_type_id' => $vehicleType->id,
                    'currency_id' => $vehicleType->currency_id,
                    'fare_amount' => $fareEach,
                    'booking_number' => 'BK-'.now()->format('Ymd').'-'.strtoupper(Str::random(5)),
                ]);

                foreach ($stops as $index => $address) {
                    if (trim($address) !== '') {
                        $booking->stops()->create(['sequence' => $index + 1, 'address' => $address]);
                    }
                }

                $booking->statusHistory()->create([
                    'status_id' => $statusId,
                    'note' => 'Booking created',
                    'changed_by' => $request->user()->id,
                ]);

                $booking->load('customer', 'company', 'driver.user', 'vehicleType.currency');

                if ($sendSms) {
                    $sms->sendBookingLink($booking);
                }

                if ($booking->driver?->user) {
                    $fcm->sendToOwner($booking->driver->user, 'New job assigned', 'Booking '.$booking->booking_number.' has been assigned to you.',
                        $this->driverJobPayload($booking, 'booking_assigned'));
                }

                if ($booking->driver_id) {
                    ExpireBookingAssignment::dispatch($booking->id)->delay(now()->addSeconds(40));
                }

                $bookings->push($booking);
                $fareTotal += $fareEach;
            }
        }

        $first = $bookings->first();
        $currencySymbol = $first->vehicleType->currency->symbol ?? '';

        $message = $bookings->count() > 1
            ? $bookings->count().' bookings created ('.implode(', ', $summaryParts).') — total fare '.$currencySymbol.number_format($fareTotal, 2).'.'
            : 'Booking created — fare quoted at '.$currencySymbol.number_format($fareTotal, 2).'.';

        return redirect()->route('admin.bookings.edit', $first)->with('success', $message);
    }

    public function edit(Request $request, Booking $booking): View
    {
        $this->authorizeCompanyOwnership($booking);

        $companyId = $request->user()->company_id ?: $booking->company_id;

        return view('admin.bookings.edit', [
            'booking' => $booking->load(['customer', 'driver', 'vehicleType.currency', 'status', 'paymentMethod', 'statusHistory.status', 'statusHistory.changedBy']),
            'drivers' => Driver::where('company_id', $companyId)->where('status', 'active')->orderBy('first_name')->get(),
            'statuses' => BookingStatus::orderBy('sort_order')->get(),
            'paymentMethods' => PaymentMethod::where('is_active', true)->orderBy('name')->get(),
            'siblings' => $booking->group_id
                ? Booking::where('group_id', $booking->group_id)->where('id', '!=', $booking->id)->with('vehicleType')->get()
                : collect(),
        ]);
    }

    /**
     * Full trip details for prefilling the dispatch console's offcanvas in
     * edit mode — same shape as rebookData() plus the fields a rebook
     * deliberately drops (driver_id, pickup_datetime), since editing an
     * existing job should show its actual current dispatch state.
     */
    public function editFormData(Request $request, Booking $booking): JsonResponse
    {
        $this->authorizeCompanyOwnership($booking);
        $booking->load('stops');

        return response()->json([
            'id' => $booking->id,
            'customer_id' => $booking->customer_id,
            'pickup_address' => $booking->pickup_address,
            'pickup_lat' => $booking->pickup_lat,
            'pickup_lng' => $booking->pickup_lng,
            'dropoff_address' => $booking->dropoff_address,
            'dropoff_lat' => $booking->dropoff_lat,
            'dropoff_lng' => $booking->dropoff_lng,
            'stops' => $booking->stops->pluck('address'),
            'vehicle_type_id' => $booking->vehicle_type_id,
            'pickup_datetime' => $booking->pickup_datetime->format('Y-m-d\TH:i'),
            'passengers' => $booking->passengers,
            'luggage' => $booking->luggage,
            'flight_number' => $booking->flight_number,
            'customer_reference' => $booking->customer_reference,
            'notes' => $booking->notes,
            'payment_method_id' => $booking->payment_method_id,
            'account_id' => $booking->account_id,
            'driver_id' => $booking->driver_id,
        ]);
    }

    /**
     * FCM `data` payload for driver job notifications (assigned / reassigned /
     * cancelled) — mirrors the old app's rich notification shape (customer,
     * vehicle, stops and booking detail alongside the title/body) so the
     * driver app can render a full job card straight from the push payload
     * instead of round-tripping to the API. FcmService::sendToTokens() runs
     * every data value through array_map('strval', ...), so anything that
     * isn't already a string here must be pre-encoded — hence json_encode()
     * on the nested objects rather than passing arrays directly.
     */
    private function driverJobPayload(Booking $booking, string $type): array
    {
        $booking->loadMissing('customer', 'vehicleType.currency', 'stops', 'status');

        return [
            'type' => $type,
            'booking_id' => (string) $booking->id,
            'booking_number' => $booking->booking_number,
            'booking' => json_encode([
                'id' => $booking->id,
                'booking_number' => $booking->booking_number,
                'status' => $booking->status->name ?? null,
                'pickup_address' => $booking->pickup_address,
                'pickup_lat' => $booking->pickup_lat,
                'pickup_lng' => $booking->pickup_lng,
                'dropoff_address' => $booking->dropoff_address,
                'dropoff_lat' => $booking->dropoff_lat,
                'dropoff_lng' => $booking->dropoff_lng,
                'pickup_datetime' => $booking->pickup_datetime?->toDateTimeString(),
                'passengers' => $booking->passengers,
                'luggage' => $booking->luggage,
                'flight_number' => $booking->flight_number,
                'customer_reference' => $booking->customer_reference,
                'notes' => $booking->notes,
                'fare_amount' => $booking->fare_amount,
                'currency_symbol' => $booking->vehicleType->currency->symbol ?? null,
            ]),
            'customer' => json_encode([
                'id' => $booking->customer?->id,
                'name' => $booking->customer?->name,
                'phone' => $booking->customer?->phone,
                'email' => $booking->customer?->email,
            ]),
            'vehicle' => json_encode([
                'id' => $booking->vehicleType?->id,
                'name' => $booking->vehicleType?->name,
                'capacity' => $booking->vehicleType?->capacity,
                'image_path' => $booking->vehicleType?->image_path,
            ]),
            'stops' => json_encode(
                $booking->stops->map(fn ($stop) => ['sequence' => $stop->sequence, 'address' => $stop->address])->values()
            ),
        ];
    }

    public function update(BookingDispatchRequest $request, Booking $booking, FcmService $fcm, BookingAssignmentService $assignment, ReceiptService $receipts): RedirectResponse
    {
        $this->authorizeCompanyOwnership($booking);

        $data = $request->validated();
        $note = $data['note'] ?? null;
        unset($data['note']);

        // Falsy-empty ('' from the "— Unassigned —" option) still counts as
        // a change when the booking currently has a driver — this is what
        // makes reassign-to-empty (the "retrieve"/unassign action) actually
        // register, unlike a plain truthy check.
        $driverChanged = ($data['driver_id'] ?: null) != $booking->driver_id;
        $driverAssigned = $data['driver_id'] && ! $booking->driver_id;
        $oldDriverUser = $driverChanged ? $booking->driver?->user : null;

        // Retrieving (unassigning or reassigning) a job that already has a
        // driver is only allowed while that driver hasn't started it yet.
        if ($driverChanged && $booking->driver_id && ! in_array($booking->status->name, ['Pending', 'Accepted'], true)) {
            return back()->withInput()->with('error', "This job can't be retrieved once it's {$booking->status->name}.");
        }

        if ($driverChanged && $data['driver_id']) {
            $driver = Driver::findOrFail($data['driver_id']);
            $this->authorizeCompanyOwnership($driver);

            if ($reason = $assignment->blockingReason($driver, $booking->id)) {
                return back()->withInput()->with('error', $reason);
            }
        }

        // Any driver change restarts the accept/reject clock (or clears it
        // entirely on unassign) — whatever status_id the form posted is
        // overridden here so the two can never drift apart.
        if ($driverChanged) {
            $data['status_id'] = $data['driver_id']
                ? BookingStatus::where('name', 'Pending')->value('id')
                : BookingStatus::where('name', 'Unassigned')->value('id');
        }

        $statusChanged = $data['status_id'] != $booking->status_id;

        $booking->update($data + [
            'assigned_by' => $driverAssigned ? $request->user()->id : $booking->assigned_by,
            'assigned_at' => $driverChanged ? now() : $booking->assigned_at,
        ]);

        if ($driverChanged && $data['driver_id']) {
            ExpireBookingAssignment::dispatch($booking->id)->delay(now()->addSeconds(40));
        }

        if ($statusChanged || $note) {
            $booking->statusHistory()->create([
                'status_id' => $data['status_id'],
                'note' => $note,
                'changed_by' => $request->user()->id,
            ]);
        }

        if ($driverChanged && ($driverUser = $booking->driver?->user)) {
            $fcm->sendToOwner($driverUser, 'New job assigned', 'Booking '.$booking->booking_number.' has been assigned to you.',
                $this->driverJobPayload($booking, 'booking_assigned'));
        }

        if ($driverChanged && $oldDriverUser) {
            $fcm->sendToOwner($oldDriverUser, 'Job reassigned', 'Booking '.$booking->booking_number.' has been reassigned to another driver and is no longer on your schedule.',
                $this->driverJobPayload($booking, 'booking_reassigned'));
        }

        if ($statusChanged && $booking->status->name === 'Cancelled' && ($driverUser = $booking->driver?->user)) {
            $fcm->sendToOwner($driverUser, 'Job cancelled', 'Booking '.$booking->booking_number.' has been cancelled.',
                $this->driverJobPayload($booking, 'booking_cancelled'));
        }

        if ($statusChanged && $booking->customer) {
            $fcm->sendToOwner($booking->customer, 'Booking update', 'Your booking '.$booking->booking_number.' is now '.$booking->status->name.'.', [
                'type' => 'booking_status_changed', 'booking_id' => (string) $booking->id,
            ]);
        }

        if ($statusChanged && $booking->status->name === 'Completed') {
            $receipts->emailToCustomer($booking);
        }

        return redirect()->route('admin.bookings.edit', $booking)->with('success', 'Booking updated.');
    }

    /**
     * Full trip-detail edit from the dispatch console's offcanvas (customer,
     * route, stops, vehicle, passenger details, payment/account, driver) —
     * distinct from update() above, which only handles the plain edit
     * page's driver/status/payment/note dispatch fields. A driver change
     * here follows the same Unassigned/Pending rules as everywhere else —
     * cleared to Unassigned, (re)assigned restarts the 40-second Pending
     * hold — everything else about status is left alone.
     */
    public function updateFull(BookingFullUpdateRequest $request, Booking $booking, GoogleMapsService $maps, FareCalculator $fares, BookingAssignmentService $assignment, FcmService $fcm): RedirectResponse
    {
        $this->authorizeCompanyOwnership($booking);
        $data = $request->validated();

        $company = $booking->company;
        $unit = $company->distance_unit ?? 'mi';

        try {
            $trip = $maps->distanceAndDuration($data['pickup_address'], $data['dropoff_address'], $unit);
        } catch (\Throwable $e) {
            return back()->withInput()->with('error', 'Could not calculate the route: '.$e->getMessage());
        }

        $driverChanged = ($data['driver_id'] ?? null) != $booking->driver_id;
        $oldDriverUser = $driverChanged ? $booking->driver?->user : null;

        // Retrieving (unassigning or reassigning) a job that already has a
        // driver is only allowed while that driver hasn't started it yet.
        if ($driverChanged && $booking->driver_id && ! in_array($booking->status->name, ['Pending', 'Accepted'], true)) {
            return back()->withInput()->with('error', "This job can't be retrieved once it's {$booking->status->name}.");
        }

        if ($driverChanged && ! empty($data['driver_id'])) {
            $driver = Driver::findOrFail($data['driver_id']);
            $this->authorizeCompanyOwnership($driver);

            if ($reason = $assignment->blockingReason($driver, $booking->id)) {
                return back()->withInput()->with('error', $reason);
            }
        }

        if (($data['account_id'] ?? null) === '__new__') {
            $data['account_id'] = Account::create([
                'company_id' => $company->id,
                'name' => $data['new_account_name'],
                'account_number' => $data['new_account_number'] ?? null,
            ])->id;
        }

        if ($data['customer_id'] === '__new__') {
            $data['customer_id'] = Customer::firstOrCreate(
                ['company_id' => $company->id, 'phone' => $data['quick_customer_phone']],
                ['name' => $data['quick_customer_name'], 'email' => $data['quick_customer_email'] ?? null, 'status' => 'active']
            )->id;
        }

        $vehicleType = VehicleType::findOrFail($data['vehicle_type_id']);
        $fareOverride = $data['fare_override'] ?? null;
        $fareAmount = $fareOverride !== null && $fareOverride !== ''
            ? round((float) $fareOverride, 2)
            : $fares->calculate($vehicleType, $trip['distance']);

        $booking->update([
            'customer_id' => $data['customer_id'],
            'account_id' => $data['account_id'] ?? null,
            'payment_method_id' => $data['payment_method_id'] ?? null,
            'driver_id' => $data['driver_id'] ?? null,
            'vehicle_type_id' => $vehicleType->id,
            'currency_id' => $vehicleType->currency_id,
            'pickup_address' => $data['pickup_address'],
            'pickup_lat' => $data['pickup_lat'] ?? null,
            'pickup_lng' => $data['pickup_lng'] ?? null,
            'dropoff_address' => $data['dropoff_address'],
            'dropoff_lat' => $data['dropoff_lat'] ?? null,
            'dropoff_lng' => $data['dropoff_lng'] ?? null,
            'pickup_datetime' => $data['pickup_datetime'],
            'passengers' => $data['passengers'],
            'luggage' => $data['luggage'] ?? 0,
            'flight_number' => $data['flight_number'] ?? null,
            'customer_reference' => $data['customer_reference'] ?? null,
            'notes' => $data['notes'] ?? null,
            'distance' => $trip['distance'],
            'distance_unit' => $unit,
            'duration_minutes' => $trip['duration_minutes'],
            'fare_amount' => $fareAmount,
            'status_id' => $driverChanged
                ? BookingStatus::where('name', ! empty($data['driver_id']) ? 'Pending' : 'Unassigned')->value('id')
                : $booking->status_id,
            'assigned_by' => $driverChanged && ! empty($data['driver_id']) ? $request->user()->id : $booking->assigned_by,
            'assigned_at' => $driverChanged ? now() : $booking->assigned_at,
        ]);

        if ($driverChanged && ! empty($data['driver_id'])) {
            ExpireBookingAssignment::dispatch($booking->id)->delay(now()->addSeconds(40));
        }

        // Stops aren't individually tracked/diffed — a full edit just
        // replaces the set wholesale, same as re-entering them from scratch.
        $booking->stops()->delete();
        foreach (($data['stops'] ?? []) as $index => $address) {
            if (trim($address) !== '') {
                $booking->stops()->create(['sequence' => $index + 1, 'address' => $address]);
            }
        }

        $booking->statusHistory()->create([
            'status_id' => $booking->status_id,
            'note' => 'Booking details edited',
            'changed_by' => $request->user()->id,
        ]);

        if ($driverChanged && ($driverUser = $booking->driver?->user)) {
            $fcm->sendToOwner($driverUser, 'New job assigned', 'Booking '.$booking->booking_number.' has been assigned to you.',
                $this->driverJobPayload($booking, 'booking_assigned'));
        }

        if ($driverChanged && $oldDriverUser) {
            $fcm->sendToOwner($oldDriverUser, 'Job reassigned', 'Booking '.$booking->booking_number.' has been reassigned to another driver and is no longer on your schedule.',
                $this->driverJobPayload($booking, 'booking_reassigned'));
        }

        return redirect()->route('admin.bookings.edit', $booking)
            ->with('success', 'Booking updated — fare quoted at '.($vehicleType->currency->symbol ?? '').number_format($fareAmount, 2).'.');
    }

    public function receipt(Booking $booking, ReceiptService $receipts)
    {
        $this->authorizeCompanyOwnership($booking);

        return $receipts->pdf($booking)->download('receipt-'.$booking->booking_number.'.pdf');
    }

    public function destroy(Booking $booking): RedirectResponse
    {
        $this->authorizeCompanyOwnership($booking);

        $booking->delete();

        return redirect()->route('admin.bookings.index')->with('success', 'Booking deleted.');
    }
}
