<?php

namespace Modules\Booking\Actions;

use Illuminate\Support\Facades\DB;
use Modules\Booking\Data\CreateBookingData;
use Modules\Booking\Data\VehicleSelectionData;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Events\BookingCreated;
use Modules\Booking\Exceptions\InvalidReturnRouteException;
use Modules\Booking\Models\Booking;
use Modules\Booking\Services\BookingRefGenerator;
use Modules\Booking\Services\BookingService;
use Modules\Routing\Models\EvRoute;
use Modules\Routing\Services\PricingService;
use Modules\Shared\Enums\VehicleOption;

class CreateBookingAction
{
    public function __construct(
        private BookingService $bookingService,
        private PricingService $pricingService,
        private BookingRefGenerator $bookingRefGenerator,
    ) {}

    public function handle(CreateBookingData $data): Booking
    {
        $this->bookingService->validateSelections($data->selections);

        $isRoundTrip = $data->returnEvRouteId !== null;

        if ($isRoundTrip) {
            $this->bookingService->validateSelections($data->returnSelections);
        }

        return DB::transaction(function () use ($data, $isRoundTrip) {
            $outboundRoute = EvRoute::findOrFail($data->evRouteId);

            $outboundBooking = $this->createLeg(
                data: $data,
                route: $outboundRoute,
                selections: $data->selections,
                travelDate: $data->travelDate,
                timeSlotId: $data->departureTimeSlotId,
                isReturnLeg: false,
            );

            if (! $isRoundTrip) {
                BookingCreated::dispatch($outboundBooking);

                return $outboundBooking;
            }

            $returnRoute = EvRoute::findOrFail($data->returnEvRouteId);

            if (! $returnRoute->isReverseOf($outboundRoute)) {
                throw InvalidReturnRouteException::notReverseOfOutbound($returnRoute, $outboundRoute);
            }

            $returnBooking = $this->createLeg(
                data: $data,
                route: $returnRoute,
                selections: $data->returnSelections,
                travelDate: $data->returnTravelDate,
                timeSlotId: $data->returnDepartureTimeSlotId,
                isReturnLeg: true,
            );

            // Linked bidirectionally after both rows exist — a single
            // `linked_booking_id` FK can't be set on either row at create
            // time since the other side doesn't have an id yet.
            $returnBooking->update(['linked_booking_id' => $outboundBooking->id]);
            $outboundBooking->update(['linked_booking_id' => $returnBooking->id]);

            // No registered listeners on BookingCreated today, so firing it
            // twice per round-trip creation has no side effects — flagged
            // here for whoever adds the first listener.
            BookingCreated::dispatch($outboundBooking);
            BookingCreated::dispatch($returnBooking);

            return $outboundBooking->refresh();
        });
    }

    /**
     * @param  list<VehicleSelectionData>  $selections
     */
    private function createLeg(
        CreateBookingData $data,
        EvRoute $route,
        array $selections,
        string $travelDate,
        int $timeSlotId,
        bool $isReturnLeg,
    ): Booking {
        $lines = array_map(
            fn (VehicleSelectionData $selection) => $this->priceSelection($route, $selection),
            $selections,
        );

        $totalPrice = array_reduce(
            $lines,
            fn (string $carry, array $line) => bcadd($carry, $line['line_total'], 2),
            '0.00',
        );

        $booking = Booking::create([
            'booking_ref' => $this->bookingRefGenerator->generate(),
            'user_id' => $data->userId,
            'openid' => $data->openid,
            'ev_route_id' => $route->id,
            'is_return_leg' => $isReturnLeg,
            'departure_time_slot_id' => $timeSlotId,
            'travel_date' => $travelDate,
            'passenger_name' => $data->passengerName,
            'passenger_phone' => $data->passengerPhone,
            'notes' => $data->notes,
            'pickup_address' => $data->pickupAddress,
            'pickup_lat' => $data->pickupLat,
            'pickup_lng' => $data->pickupLng,
            'dropoff_address' => $data->dropoffAddress,
            'dropoff_lat' => $data->dropoffLat,
            'dropoff_lng' => $data->dropoffLng,
            'price' => $totalPrice,
            'status' => BookingStatus::PendingPayment,
            'created_by_channel' => $data->createdByChannel,
        ]);

        $booking->vehicleOptions()->createMany($lines);

        return $booking;
    }

    /**
     * @return array{vehicle_option: VehicleOption, passenger_count: int, unit_price: string, line_total: string}
     */
    private function priceSelection(EvRoute $route, VehicleSelectionData $selection): array
    {
        $quote = $this->pricingService->quote($route, $selection->vehicleOption);

        return [
            'vehicle_option' => $selection->vehicleOption,
            'passenger_count' => $selection->passengerCount,
            'unit_price' => $quote->price,
            'line_total' => bcmul($quote->price, (string) $selection->passengerCount, 2),
        ];
    }
}
