<?php

namespace App\Services\Shipment;

use App\Exceptions\CarrierCreateFailedException;
use App\Models\Carrier;
use App\Models\Order;
use App\Models\Shipment;
use App\Services\Carriers\CarrierServiceFactory;
use App\Services\OrderTrackingNotificationService;
use Illuminate\Support\Facades\Log;
use Throwable;

class ShipmentBookingService
{
    public function __construct(
        private readonly ShipmentCarrierFallbackService $fallbackService,
        private readonly ShipmentStatusSyncService $statusSync,
        private readonly OrderTrackingNotificationService $trackingNotifier
    ) {}

    /**
     * @param  callable(Carrier, Order, array): array|null  $createShipment
     */
    public function book(Shipment $shipment, array $formData = [], ?callable $createShipment = null): void
    {
        $createShipment ??= function (Carrier $carrier, Order $order, array $payload): array {
            return CarrierServiceFactory::makeFromModel($carrier)->createShipment($order, $payload);
        };

        $shipment = $shipment->fresh(['carrier', 'order']);
        if (!$shipment || !$shipment->order || !$shipment->carrier) {
            throw new \RuntimeException('Shipment, order, or carrier is missing.');
        }

        $order = $shipment->order;
        $originalOrderStatus = $order->status;
        $primaryCarrier = $shipment->carrier;

        $carriers = [$primaryCarrier];
        $fallbackCarrier = $this->fallbackService->fallbackCarrier($primaryCarrier);
        if ($fallbackCarrier) {
            $carriers[] = $fallbackCarrier;
        }

        $shipment->update(['status' => Shipment::STATUS_PENDING]);

        $attemptErrors = [];
        $lastException = null;

        foreach ($carriers as $index => $carrier) {
            $isFallback = $index > 0;

            if ($isFallback) {
                $shipment->update(['carrier_id' => $carrier->id]);
                $shipment->setRelation('carrier', $carrier);
            }

            try {
                $result = $createShipment($carrier, $order, $formData);
                $trackingId = $result['tracking_id'] ?? null;

                if ($trackingId === null || $trackingId === '') {
                    $attemptErrors[] = [
                        'carrier' => $carrier->slug,
                        'error' => 'No AWB/tracking_id returned from carrier',
                        'result' => $result,
                    ];
                    Log::error('ShipmentBookingService: Shipment create failed (no AWB)', [
                        'shipment_id' => $shipment->id,
                        'order_id' => $order->id,
                        'carrier' => $carrier->slug,
                        'result' => $result,
                    ]);
                    continue;
                }

                $this->markCreated($shipment, $order, $result, (string) $trackingId, $isFallback, $primaryCarrier, $carrier, $attemptErrors);

                return;
            } catch (CarrierCreateFailedException $e) {
                $attemptErrors[] = [
                    'carrier' => $carrier->slug,
                    'error' => $e->getMessage(),
                    'failure_reason' => $e->getFailureReason(),
                    'raw_response' => $e->getRawResponse(),
                ];
                $lastException = $e;

                Log::warning('ShipmentBookingService: Carrier create failed', [
                    'shipment_id' => $shipment->id,
                    'order_id' => $order->id,
                    'carrier' => $carrier->slug,
                    'error' => $e->getMessage(),
                    'failure_reason' => $e->getFailureReason(),
                    'will_try_fallback' => $isFallback === false && $fallbackCarrier !== null,
                ]);
            } catch (Throwable $e) {
                $attemptErrors[] = [
                    'carrier' => $carrier->slug,
                    'error' => $e->getMessage(),
                ];
                $lastException = $e;

                Log::warning('ShipmentBookingService: Carrier create exception', [
                    'shipment_id' => $shipment->id,
                    'order_id' => $order->id,
                    'carrier' => $carrier->slug,
                    'error' => $e->getMessage(),
                    'will_try_fallback' => $isFallback === false && $fallbackCarrier !== null,
                ]);
            }
        }

        $this->failWithoutChangingOrderStatus($shipment, $order, $originalOrderStatus, $attemptErrors, $lastException);
    }

    private function markCreated(
        Shipment $shipment,
        Order $order,
        array $result,
        string $trackingId,
        bool $isFallback,
        Carrier $primaryCarrier,
        Carrier $usedCarrier,
        array $attemptErrors
    ): void {
        $metadata = array_merge($shipment->metadata ?? [], $result['metadata'] ?? []);
        if ($isFallback) {
            $metadata['fallback'] = [
                'from' => $primaryCarrier->slug,
                'to' => $usedCarrier->slug,
                'primary_errors' => $attemptErrors,
            ];
        }

        $shipment->update([
            'tracking_id' => $trackingId,
            'status' => Shipment::STATUS_CREATED,
            'label_url' => $result['label_url'] ?? null,
            'raw_response' => $result['raw_response'] ?? null,
            'metadata' => $metadata,
        ]);

        $this->statusSync->applyCourierStatus($shipment->fresh(), Shipment::STATUS_CREATED, [
            'source' => $isFallback ? 'shipment_create_fallback' : 'shipment_create',
            'carrier' => $usedCarrier->slug,
        ]);

        $order->update([
            'delhivery_awb' => $trackingId,
            'delhivery_label_url' => $result['label_url'] ?? null,
        ]);

        $order->refresh();
        if ($order->status === Order::STATUS_AWAITING_PICKUP) {
            $this->trackingNotifier->sendIfEnabled($order);
        }

        Log::info('ShipmentBookingService: Shipment processed successfully', [
            'shipment_id' => $shipment->id,
            'tracking_id' => $trackingId,
            'carrier' => $usedCarrier->slug,
            'fallback' => $isFallback,
        ]);
    }

    private function failWithoutChangingOrderStatus(
        Shipment $shipment,
        Order $order,
        string $originalOrderStatus,
        array $attemptErrors,
        ?Throwable $lastException
    ): void {
        $carrierNames = collect($attemptErrors)->pluck('carrier')->filter()->unique()->implode(' and ');
        $combinedMessage = $carrierNames !== ''
            ? "Shipment create failed for {$carrierNames}."
            : 'Shipment create failed.';

        if ($lastException) {
            $combinedMessage .= ' ' . $lastException->getMessage();
        }

        $shipment->update([
            'status' => Shipment::STATUS_FAILED,
            'raw_response' => [
                'error' => $combinedMessage,
                'attempts' => $attemptErrors,
            ],
            'metadata' => array_merge($shipment->metadata ?? [], [
                'failure_reason' => $lastException instanceof CarrierCreateFailedException
                    ? $lastException->getFailureReason()
                    : CarrierCreateFailedException::FAILURE_UNKNOWN,
                'attempt_errors' => $attemptErrors,
                'order_status_preserved' => $originalOrderStatus,
            ]),
        ]);

        $order->refresh();
        if ($order->status !== $originalOrderStatus) {
            $order->update(['status' => $originalOrderStatus]);
        }

        Log::error('ShipmentBookingService: All carrier create attempts failed', [
            'shipment_id' => $shipment->id,
            'order_id' => $order->id,
            'original_order_status' => $originalOrderStatus,
            'attempts' => $attemptErrors,
        ]);

        if ($lastException instanceof CarrierCreateFailedException) {
            throw new CarrierCreateFailedException(
                $combinedMessage,
                0,
                $lastException,
                [
                    'attempts' => $attemptErrors,
                    'raw_response' => $lastException->getRawResponse(),
                ],
                $lastException->getFailureReason()
            );
        }

        if ($lastException instanceof Throwable) {
            throw $lastException;
        }

        throw new CarrierCreateFailedException(
            $combinedMessage,
            0,
            null,
            ['attempts' => $attemptErrors],
            CarrierCreateFailedException::FAILURE_UNKNOWN
        );
    }
}
