<?php

namespace App\Jobs;

use App\Facades\Settings;
use App\Services\AutoTraderApi;
use App\Services\AutoTraderStockWriter;
use App\Services\AutoTraderVehicleMapper;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\Response;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Intervention\Image\ImageManager;
use Mtc\MercuryDataModels\ApiNotification;
use Mtc\MercuryDataModels\Vehicle;

/**
 * Sends a vehicle to the AutoTrader Stock API in-process (POST/PATCH) or removes it (PATCH to
 * DELETED). Ported from the standalone autotrader-microservice SendVehicleToAutoTrader job so that
 * outgoing requests are logged per-vehicle (api_notifications) and can be skipped when unchanged.
 *
 * Reached via App\Services\AutoTraderHub when the `sales-channels-auto-trader-in-process-export`
 * setting is enabled.
 */
class SendVehicleToAutotrader implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public $timeout = 180;
    public $tries = 3;

    private const PROVIDER = 'auto-trader-export';
    private const BODY_TYPE_SKIP_LIST = ['Motorhome', 'Caravan', 'Semi-Integrated'];
    private const NOT_PUBLISHED_LIFECYCLE_STATES = ['SOLD', 'DELETED', 'WASTEBIN'];

    private const EXPORT_RELATIONS = [
        'make',
        'model',
        'transmission',
        'fuelType',
        'bodyStyle',
        'drivetrain',
        'features',
        'equipment',
        'mediaUses.media',
        'dealership',
        'autoTraderData',
    ];

    private AutoTraderVehicleMapper $mapper;
    private ?AutoTraderStockWriter $writer = null;
    private array $uploadedImages = [];
    private array $recovery = [];
    private ?string $oldStockId = null;
    private ?array $derivativeDetails = null;

    public function __construct(
        private readonly Vehicle $vehicle,
        private readonly bool $remove = false,
    ) {
    }

    public function handle(): void
    {
        $this->vehicle->loadMissing(self::EXPORT_RELATIONS);
        $this->mapper = new AutoTraderVehicleMapper();

        $advertiserId = $this->vehicle->dealership?->data['auto-trader-location-id'] ?? null;

        if ($this->remove) {
            $this->handleRemove($advertiserId);
            return;
        }

        if (!$this->canProceed()) {
            return;
        }

        if (empty($advertiserId)) {
            $this->markSkipped('no advertiser id');
            return;
        }

        $this->writer = $this->writerFor($advertiserId);

        if (!$this->skipsDerivativeIdentification()) {
            $this->findDerivativeId($advertiserId);
            if (empty($this->vehicle->autoTraderData?->derivative_id) && empty($this->derivativeDetails['derivativeId'])) {
                $this->handleMissingDerivative();
                return;
            }
        }

        $payload = $this->buildPayload();

        if ($this->shouldSkipUnchanged($payload)) {
            return;
        }

        $this->uploadImages();
        $this->vehicle->load('mediaUses.media');

        $this->send($this->buildPayload());
    }

    private function send(array $payload): void
    {
        if (!empty($this->vehicle->auto_trader_id)) {
            $requestType = 'patch';
            $response = $this->writer->patchVehicle($this->vehicle->auto_trader_id, $payload);

            if ($this->hasStockNotFoundError($response)) {
                $this->recovery[] = 'stock-not-found: demote to post';
                $this->vehicle->auto_trader_id = null;
                $this->vehicle->save();
                $this->send($payload);
                return;
            }
        } else {
            $requestType = 'post';
            $response = $this->writer->postVehicle($payload);

            if ($this->hasDuplicateStockError($response)) {
                $duplicateStockId = $this->getDuplicateStockId($response->json());
                if ($duplicateStockId) {
                    $existing = $this->writer->findStockId($duplicateStockId);
                    $existingDerivative = $existing['vehicle']['derivativeId'] ?? null;
                    if (($payload['vehicle']['derivativeId'] ?? null) === $existingDerivative) {
                        $this->recovery[] = 'duplicate-stock: retry as patch';
                        $this->vehicle->auto_trader_id = $duplicateStockId;
                        $this->vehicle->save();
                        $this->send($payload);
                        return;
                    }
                }
            }
        }

        if ($this->hasStockHeaderMismatchError($response)) {
            $oldAdvertiserId = $this->getStockMismatchDid($response->json());
            if ($oldAdvertiserId) {
                $this->recovery[] = 'header-mismatch: delete old advertiser stock';
                $this->oldStockId = $this->vehicle->auto_trader_id;
                $this->vehicle->auto_trader_id = null;
                $this->vehicle->save();
                $this->deleteOldAdvertiserIdStock($oldAdvertiserId);
                $this->send($payload);
                return;
            }
        }

        $this->finalise($payload, $response, $requestType);
    }

    private function finalise(array $payload, Response $response, string $requestType): void
    {
        $stockId = $response->json('metadata.stockId') ?: $this->vehicle->auto_trader_id;
        $notification = $this->logRequest($requestType, $payload, $response);

        if ($response->successful()) {
            new HandleAutoTraderHubNotification([
                'result' => 'success',
                'type' => 'stock-upsert',
                'vehicle_id' => $this->vehicle->id,
                'stock_id' => $stockId,
                'data' => $response->json(),
                'uploaded_images' => $this->uploadedImages,
                'old_stock_id' => $this->oldStockId,
            ], $notification)->handle();

            $this->storeExportSnapshot($payload);
        } else {
            new HandleAutoTraderHubNotification([
                'result' => 'error',
                'type' => 'stock-upsert',
                'vehicle_id' => $this->vehicle->id,
                'stock_id' => $stockId,
                'data' => $response->json(),
            ], $notification)->handle();
        }
    }

    private function handleRemove(?string $advertiserId): void
    {
        if (empty($this->vehicle->auto_trader_id)) {
            return;
        }

        $advertiserId = $advertiserId ?: (string) Settings::get('auto-trader-advertiser-id');
        if (empty($advertiserId)) {
            $this->markSkipped('no advertiser id for removal');
            return;
        }

        $this->writer = $this->writerFor($advertiserId);
        $payload = $this->removalPayload();
        $response = $this->writer->patchVehicle($this->vehicle->auto_trader_id, $payload);
        $this->logRequest('remove', $payload, $response);

        if ($this->vehicle->autoTraderData) {
            $this->vehicle->autoTraderData()->update(['lifecycle_state' => 'DELETED']);
        }
    }

    private function handleMissingDerivative(): void
    {
        $notification = $this->logRequest('upsert-error', [], null);

        new HandleAutoTraderHubNotification([
            'result' => 'error',
            'type' => 'upsert-error',
            'vehicle_id' => $this->vehicle->id,
            'error' => 'missing_derivative',
        ], $notification)->handle();
    }

    private function buildPayload(): array
    {
        $data = $this->mapper->mapVehicle($this->vehicle);

        if ($this->derivativeDetails) {
            $overrides = [
                'make' => 'make',
                'model' => 'model',
                'body_type' => 'bodyType',
                'drivetrain' => 'drivetrain',
                'transmission_type' => 'transmissionType',
                'fuel_type' => 'fuelType',
                'derivative_id' => 'derivativeId',
            ];

            foreach ($overrides as $dataKey => $detailKey) {
                if (array_key_exists($dataKey, $data) && !empty($this->derivativeDetails[$detailKey])) {
                    $data[$dataKey] = $this->derivativeDetails[$detailKey];
                }
            }
        }

        return $this->mapper->toApiSchema($data);
    }

    private function shouldSkipUnchanged(array $payload): bool
    {
        if ($this->isForcedSend($payload)) {
            return false;
        }

        if ($this->vehicle->autoTraderData?->last_export_hash === $this->hash($payload)) {
            $this->logRequest('skipped-unchanged', $payload, null);
            return true;
        }

        return false;
    }

    private function isForcedSend(array $payload): bool
    {
        if (empty($this->vehicle->auto_trader_id)) {
            return true;
        }

        if (!empty($this->vehicle->autoTraderData?->at_export_error)) {
            return true;
        }

        $lifecycle = $payload['metadata']['lifecycleState'] ?? null;

        return in_array($lifecycle, self::NOT_PUBLISHED_LIFECYCLE_STATES, true);
    }

    private function storeExportSnapshot(array $payload): void
    {
        $this->vehicle->autoTraderData()->updateOrCreate(
            ['vehicle_id' => $this->vehicle->id],
            [
                'last_export_hash' => $this->hash($payload),
                'last_export_payload' => $payload,
            ]
        );
    }

    private function findDerivativeId(string $advertiserId): void
    {
        if (!empty($this->vehicle->autoTraderData?->derivative_id)) {
            return;
        }

        if (!empty($this->vehicle->vin)) {
            $details = $this->writer->findVehicleDetails($this->vehicle->vin, $advertiserId, 'vin');
        } elseif (!empty($this->vehicle->registration_number)) {
            $details = $this->writer->findVehicleDetails($this->vehicle->registration_number, $advertiserId);
        }

        if (!empty($details['derivativeId'])) {
            $this->derivativeDetails = $details;
            $this->vehicle->autoTraderData()->updateOrCreate(
                ['vehicle_id' => $this->vehicle->id],
                ['derivative_id' => $details['derivativeId']]
            );
            $this->vehicle->load('autoTraderData');
        }
    }

    private function uploadImages(): void
    {
        foreach ($this->vehicle->mediaUses as $mediaUse) {
            $media = $mediaUse->media;
            if (!$media || !empty($media->auto_trader_id)) {
                continue;
            }

            $url = $media->getOriginalUrlAttribute(true);
            if (empty($url)) {
                continue;
            }

            try {
                $image = (new ImageManager(Config::get('media.image_manager_config', [])))
                    ->make(file_get_contents($url));
                $response = $this->writer->uploadImage($image->stream('jpg'), $this->imageFileName($url));
                $imageId = $response->json('imageId');

                if (!empty($imageId)) {
                    $media->update([
                        'auto_trader_id' => $imageId,
                        'image_provider' => $media->image_provider ?? 'auto-trader',
                    ]);
                    $this->uploadedImages[] = ['media_id' => $media->id, 'auto_trader_id' => $imageId];
                }
            } catch (\Throwable $exception) {
                Log::error('AutoTrader image upload failed', [
                    'media' => $media->id,
                    'error' => $exception->getMessage(),
                ]);
            }
        }
    }

    private function deleteOldAdvertiserIdStock(string $oldAdvertiserId): void
    {
        if (empty($this->oldStockId)) {
            return;
        }

        $this->writerFor($oldAdvertiserId)
            ->patchVehicle($this->oldStockId, $this->removalPayload(), $oldAdvertiserId);
    }

    private function removalPayload(): array
    {
        $notPublished = ['status' => 'NOT_PUBLISHED'];

        return [
            'metadata' => [
                'lifecycleState' => 'DELETED',
            ],
            'adverts' => [
                'retailAdverts' => [
                    'advertiserAdvert' => $notPublished,
                    'autotraderAdvert' => $notPublished,
                    'profileAdvert' => $notPublished,
                    'locatorAdvert' => $notPublished,
                    'exportAdvert' => $notPublished,
                ],
            ],
        ];
    }

    private function canProceed(): bool
    {
        if (
            !$this->skipsDerivativeIdentification()
            && empty($this->vehicle->registration_number)
            && empty($this->vehicle->vin)
        ) {
            $this->markSkipped('no VRM or VIN');
            return false;
        }

        return true;
    }

    private function skipsDerivativeIdentification(): bool
    {
        return in_array($this->vehicle->bodyStyle?->name, self::BODY_TYPE_SKIP_LIST, true);
    }

    private function writerFor(string $advertiserId): AutoTraderStockWriter
    {
        return new AutoTraderStockWriter($advertiserId, App::make(AutoTraderApi::class));
    }

    private function imageFileName(string $url): string
    {
        $fileName = Carbon::now()->format('U') . pathinfo($url, PATHINFO_BASENAME);
        $extension = pathinfo($fileName, PATHINFO_EXTENSION);

        return Str::slug(str_replace('.' . $extension, '', Str::limit($fileName, 75))) . '.jpeg';
    }

    private function hash(array $payload): string
    {
        return md5(json_encode($payload));
    }

    private function markSkipped(string $reason): void
    {
        Log::info('SendVehicleToAutotrader skipped: ' . $reason, ['vehicle' => $this->vehicle->id]);
        $this->logRequest('skipped', [], null, ['reason' => $reason]);
    }

    private function logRequest(string $type, array $payload, ?Response $response, array $extra = []): ApiNotification
    {
        return ApiNotification::query()->create([
            'provider' => self::PROVIDER,
            'data' => array_merge([
                'request_type' => $type,
                'request' => $payload,
                'response' => $response?->json(),
                'status' => $response?->status(),
                'recovery' => $this->recovery,
            ], $extra),
            'data_model' => 'vehicle',
            'reference' => $this->vehicle->vrm_condensed,
            'processed' => $response ? $response->successful() : true,
        ]);
    }

    private function hasStockNotFoundError(Response $response): bool
    {
        return !$response->successful()
            && $this->hasStockWarning($response->json(), fn (string $message) => str_starts_with(
                $message,
                'No stock found with id'
            ));
    }

    private function hasDuplicateStockError(Response $response): bool
    {
        return $response->status() === 409
            && $this->hasStockWarning($response->json(), fn (string $message) => str_starts_with(
                $message,
                'Duplicate stock found'
            ));
    }

    private function getDuplicateStockId(array $data): ?string
    {
        foreach ($data['warnings'] ?? [] as $warning) {
            if (
                ($warning['feature'] ?? null) === 'stock'
                && str_starts_with($warning['message'] ?? '', 'Duplicate stock found')
                && preg_match('/stockId=([a-f0-9]+)/i', $warning['message'], $matches)
            ) {
                return $matches[1];
            }
        }

        return null;
    }

    private function hasStockHeaderMismatchError(Response $response): bool
    {
        return $response->status() === 403
            && $this->hasStockWarning($response->json(), fn (string $message) => str_contains(
                $message,
                "isn't equal to header did"
            ));
    }

    private function getStockMismatchDid(array $data): ?string
    {
        foreach ($data['warnings'] ?? [] as $warning) {
            if (
                ($warning['feature'] ?? null) === 'stock'
                && preg_match('/Stock did (\d+) isn\'t equal to header did \d+/', $warning['message'] ?? '', $matches)
            ) {
                return $matches[1];
            }
        }

        return null;
    }

    private function hasStockWarning(?array $data, callable $messageMatches): bool
    {
        foreach ($data['warnings'] ?? [] as $warning) {
            if (
                ($warning['type'] ?? null) === 'ERROR'
                && ($warning['feature'] ?? null) === 'stock'
                && $messageMatches($warning['message'] ?? '')
            ) {
                return true;
            }
        }

        return false;
    }
}
