<?php

namespace App\Http\Controllers\Api\Driver;

use App\Http\Controllers\Controller;
use App\Jobs\ExpireBookingAssignment;
use App\Models\Booking;
use App\Models\BookingStatus;
use App\Models\Customer;
use App\Models\Driver;
use App\Services\FcmService;
use App\Services\ReceiptService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class BookingController extends Controller
{
    /**
     * Linear per-job state machine a driver steps through — replaces the
     * old project's single update_booking_status endpoint (which packed
     * accept/arrive/onboard/complete/cancel into one action + a string
     * switch) with one small, obviously-correct method per transition.
     */
    private const TRANSITIONS = [
        'arrive' => ['from' => 'Accepted', 'to' => 'Arrived'],
        'onboard' => ['from' => 'Arrived', 'to' => 'Onboard'],
        'complete' => ['from' => 'Onboard', 'to' => 'Completed'],
    ];

    /** @var string[] statuses that mean "driver is actively working this job right now" */
    private const ACTIVE_STATUSES = ['Accepted', 'Arrived', 'Onboard'];

    public function jobs(Request $request): JsonResponse
    {
        $driver = $this->driver($request);

        $jobs = Booking::with(['customer', 'vehicleType', 'status'])
            ->where('driver_id', $driver->id)
            ->whereHas('status', fn ($q) => $q->whereIn('name', ['Pending', 'Future Job', 'Accepted', 'Arrived', 'Onboard']))
            ->orderBy('pickup_datetime')
            ->get();

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

    public function show(Request $request, Booking $booking): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);

        return response()->json(['booking' => $booking->load(['customer', 'vehicleType', 'status', 'stops'])]);
    }

    /**
     * A driver can be offered a second job while still working their
     * current one — rather than rejecting it outright, they can accept it
     * as a "Future Job" that sits queued until their active job completes
     * (or is cancelled), at which point it's automatically promoted back to
     * Pending. Only one job can be queued at a time — mirrors a driver only
     * having one van, so a second offer while already holding a queued job
     * has nowhere to go.
     */
    public function accept(Request $request, Booking $booking, FcmService $fcm): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);
        abort_unless($booking->status->name === 'Pending', 409, __('api.job_transition_invalid', ['from' => 'Pending', 'to' => 'Accepted']));

        $alreadyQueued = Booking::where('driver_id', $driver->id)
            ->where('id', '!=', $booking->id)
            ->whereHas('status', fn ($q) => $q->where('name', 'Future Job'))
            ->exists();

        abort_if($alreadyQueued, 409, __('api.driver_already_has_future_job'));

        $isBusy = Booking::where('driver_id', $driver->id)
            ->where('id', '!=', $booking->id)
            ->whereHas('status', fn ($q) => $q->whereIn('name', self::ACTIVE_STATUSES))
            ->exists();

        $toName = $isBusy ? 'Future Job' : 'Accepted';
        $toId = BookingStatus::where('name', $toName)->value('id');

        DB::transaction(function () use ($booking, $toId, $toName, $request) {
            $booking->update(['status_id' => $toId]);
            $booking->statusHistory()->create([
                'status_id' => $toId,
                'note' => $toName === 'Future Job' ? 'Driver already has an active job — queued as a future job.' : null,
                'changed_by' => $request->user()->id,
            ]);
        });

        if ($booking->customer) {
            $message = $toName === 'Future Job'
                ? 'Your driver has accepted booking '.$booking->booking_number.' — it will start once their current job finishes.'
                : 'Your booking '.$booking->booking_number.' is now Accepted.';

            $fcm->sendToOwner($booking->customer, 'Booking update', $message, [
                'type' => 'booking_status_changed', 'booking_id' => (string) $booking->id,
            ]);
        }

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

    /**
     * A driver declining a newly-assigned job — the mirror image of
     * ExpireBookingAssignment's 40-second timeout, just triggered
     * immediately instead of by silence. Logged as its own "Rejected"
     * status-history entry (the specific thing that happened) right before
     * the booking lands on its actual resulting state, Unassigned, freeing
     * it up for reassignment.
     */
    public function reject(Request $request, Booking $booking): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);
        abort_unless($booking->status->name === 'Pending', 409, __('api.job_transition_invalid', ['from' => 'Pending', 'to' => 'Rejected']));

        $data = $request->validate(['reason' => ['nullable', 'string', 'max:500']]);

        $rejectedId = BookingStatus::where('name', 'Rejected')->value('id');
        $unassignedId = BookingStatus::where('name', 'Unassigned')->value('id');

        DB::transaction(function () use ($booking, $rejectedId, $unassignedId, $data, $request) {
            $booking->statusHistory()->create([
                'status_id' => $rejectedId,
                'note' => 'Rejected by driver'.(isset($data['reason']) ? ' — '.$data['reason'] : ''),
                'changed_by' => $request->user()->id,
            ]);

            $booking->update(['status_id' => $unassignedId, 'driver_id' => null]);

            $booking->statusHistory()->create([
                'status_id' => $unassignedId,
                'note' => 'Returned to Unassigned for reassignment.',
                'changed_by' => $request->user()->id,
            ]);
        });

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

    public function arrive(Request $request, Booking $booking, FcmService $fcm): JsonResponse
    {
        return $this->transition($request, $booking, 'arrive', $fcm);
    }

    public function onboard(Request $request, Booking $booking, FcmService $fcm): JsonResponse
    {
        return $this->transition($request, $booking, 'onboard', $fcm);
    }

    public function complete(Request $request, Booking $booking, FcmService $fcm, ReceiptService $receipts): JsonResponse
    {
        $response = $this->transition($request, $booking, 'complete', $fcm);
        $receipts->emailToCustomer($booking->fresh());
        $this->releaseQueuedFutureJob($this->driver($request), $request, $fcm);

        return $response;
    }

    public function cancel(Request $request, Booking $booking, FcmService $fcm): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);
        abort_if(in_array($booking->status->name, ['Completed', 'Cancelled'], true), 409, __('api.job_not_cancellable'));

        $data = $request->validate(['reason' => ['nullable', 'string', 'max:500']]);
        $cancelledId = BookingStatus::where('name', 'Cancelled')->value('id');

        DB::transaction(function () use ($booking, $cancelledId, $data, $request) {
            $booking->update(['status_id' => $cancelledId]);
            $booking->statusHistory()->create([
                'status_id' => $cancelledId,
                'note' => 'Cancelled by driver'.(isset($data['reason']) ? ' — '.$data['reason'] : ''),
                'changed_by' => $request->user()->id,
            ]);
        });

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

        $this->releaseQueuedFutureJob($driver, $request, $fcm);

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

    /**
     * Lets a driver propose a revised fare (e.g. a detour or wait time) —
     * mirrors the old project's raise_fair_by_driver, notifying the
     * customer of the new amount rather than silently overwriting it.
     */
    public function raiseFare(Request $request, Booking $booking, FcmService $fcm): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);

        $data = $request->validate([
            'fare_amount' => ['required', 'numeric', 'min:0'],
            'reason' => ['nullable', 'string', 'max:500'],
        ]);

        $booking->update(['fare_amount' => $data['fare_amount']]);
        $booking->statusHistory()->create([
            'status_id' => $booking->status_id,
            'note' => 'Driver revised fare to '.$data['fare_amount'].(isset($data['reason']) ? ' — '.$data['reason'] : ''),
            'changed_by' => $request->user()->id,
        ]);

        if ($booking->customer) {
            $fcm->sendToOwner($booking->customer, 'Fare updated', 'Your driver revised the fare for '.$booking->booking_number.' to '.$data['fare_amount'].'.', [
                'type' => 'fare_revised', 'booking_id' => (string) $booking->id,
            ]);
        }

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

    /**
     * ADD-01: driver rates the passenger after completing a job. Same
     * one-per-booking-per-rater rule as the customer side.
     */
    public function rate(Request $request, Booking $booking): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);
        abort_unless($booking->status->name === 'Completed', 409, __('api.rate_completed_only'));
        abort_unless($booking->customer_id, 409, __('api.no_passenger_to_rate'));

        if ($booking->ratings()->where('rater_type', Driver::class)->where('rater_id', $driver->id)->exists()) {
            return response()->json(['message' => __('api.already_rated')], 409);
        }

        $data = $request->validate([
            'stars' => ['required', 'integer', 'min:1', 'max:5'],
            'comment' => ['nullable', 'string', 'max:500'],
        ]);

        $rating = $booking->ratings()->create([
            'rater_type' => Driver::class, 'rater_id' => $driver->id,
            'ratee_type' => Customer::class, 'ratee_id' => $booking->customer_id,
            'stars' => $data['stars'], 'comment' => $data['comment'] ?? null,
        ]);

        return response()->json(['rating' => $rating], 201);
    }

    private function transition(Request $request, Booking $booking, string $action, FcmService $fcm): JsonResponse
    {
        $driver = $this->driver($request);
        abort_unless($booking->driver_id === $driver->id, 404);

        [$from, $to] = [self::TRANSITIONS[$action]['from'], self::TRANSITIONS[$action]['to']];
        abort_unless($booking->status->name === $from, 409, __('api.job_transition_invalid', ['from' => $from, 'to' => $to]));

        $toId = BookingStatus::where('name', $to)->value('id');

        DB::transaction(function () use ($booking, $toId, $request) {
            $booking->update(['status_id' => $toId]);
            $booking->statusHistory()->create([
                'status_id' => $toId,
                'changed_by' => $request->user()->id,
            ]);
        });

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

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

    /**
     * Promotes a driver's parked "Future Job" (see accept()) back to
     * Pending once whatever was keeping them busy stops being active —
     * their current job completing or getting cancelled. That restarts the
     * same 40-second accept/reject clock as any other new assignment, since
     * the driver hasn't actually agreed to this one yet, just queued it.
     * Only ever one job can be queued at a time, so this is a lookup-and-
     * flip rather than a queue pop.
     */
    private function releaseQueuedFutureJob(Driver $driver, Request $request, FcmService $fcm): void
    {
        $queued = Booking::where('driver_id', $driver->id)
            ->whereHas('status', fn ($q) => $q->where('name', 'Future Job'))
            ->first();

        if (! $queued) {
            return;
        }

        $pendingId = BookingStatus::where('name', 'Pending')->value('id');

        DB::transaction(function () use ($queued, $pendingId, $request) {
            $queued->update(['status_id' => $pendingId, 'assigned_at' => now()]);
            $queued->statusHistory()->create([
                'status_id' => $pendingId,
                'note' => 'Promoted from Future Job — driver is now free to accept it.',
                'changed_by' => $request->user()->id,
            ]);
        });

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

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

    private function driver(Request $request): Driver
    {
        $driver = $request->user()->driver;
        abort_unless($driver, 404, __('api.no_driver_profile'));

        return $driver;
    }
}
