<?php

namespace App\Jobs;

use App\Events\AutoTraderIdAssignedToVehicle;
use App\Facades\Settings;
use App\Imports\AutoTraderApiToVehicleImport;
use App\IntegrationRepository;
use App\SalesChannelType;
use App\Services\AutoTraderDealershipResolver;
use App\Services\AutoTraderHub;
use App\Traits\UsesSalesChannelRules;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Mtc\MercuryDataModels\ApiNotification;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Media;
use Mtc\MercuryDataModels\SalesChannelHistory;
use Mtc\MercuryDataModels\Vehicle;

class HandleAutoTraderHubNotification implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;
    use UsesSalesChannelRules;

    protected AutoTraderApiToVehicleImport $import;

    /**
     * Create a new job instance.
     */
    public function __construct(
        private readonly array $input,
        private ApiNotification $apiNotification
    ) {
        $this->import = resolve(AutoTraderApiToVehicleImport::class);
    }

    public function middleware(): array
    {
        $key = 'at-hub-' . ($this->input['stock_id'] ?? 'no-stock-id');

        return [
            (new WithoutOverlapping($key))
                ->releaseAfter(5)
                ->expireAfter(60),
        ];
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        $action = $this->input['type'] ?? null;

        match ($action) {
            'upsert-error' => $this->handleUpsertError(),
            'stock-upsert' => $this->handleStockUpsert(),
            'STOCK_UPDATE' => $this->handleStockUpdate(),
            default => $this->handleUnrecognizedAction($action),
        };
    }

    private function handleStockUpsert(): void
    {
        if ($this->input['result'] === 'success') {
            $excludedVehicle = $this->vehicleNoLongerEligibleForAutoTrader();
            if ($excludedVehicle) {
                // The vehicle was sent to AutoTrader, then its VRM was changed to a value the
                // sales-channel rules exclude (e.g. a new-plate "111") before this upsert ack
                // came back. Don't link/update it from this stale ack - pull the listing AT
                // just created and skip processing.
                $this->removeVehicleFromAutoTrader($excludedVehicle);
            } else {
                $this->setAutoTraderIdForVehicle();
                $this->assignUploadedImages();
                $this->handleStockUpdate(false);
                $this->handleImageOrdering();
            }
        }

        SalesChannelHistory::query()
            ->create([
                'channel' => 'auto-trader',
                'vehicle_id' => $this->input['vehicle_id'],
                'was_successful' => $this->input['result'] === 'success',
                'details' => json_encode($this->input['data']),
            ]);
    }

    /**
     * Return the vehicle this upsert ack refers to when it no longer qualifies for the
     * AutoTrader sales channel (its current data fails the channel rules - e.g. the VRM was
     * changed to an excluded value). Returns null when it still qualifies, when there are no
     * channel rules, or when the vehicle cannot be found.
     */
    private function vehicleNoLongerEligibleForAutoTrader(): ?Vehicle
    {
        if (!Settings::get('auto-trader-hub-remove-unmatched-vehicles')) {
            return null;
        }

        if (!$this->hasSalesChannelRules(SalesChannelType::AUTOTRADER)) {
            return null;
        }

        /** @var Vehicle|null $vehicle */
        $vehicle = Vehicle::query()
            ->where('id', $this->input['vehicle_id'] ?? 0)
            ->withTrashed()
            ->first();

        if (!$vehicle) {
            return null;
        }

        $vehicle->loadMissing('attributeValues');
        $rules = $this->getSalesChannelRules(SalesChannelType::AUTOTRADER);

        return $this->applyModelConditions($vehicle, $rules) ? null : $vehicle;
    }

    /**
     * Remove the AutoTrader listing that was just (re)created for a vehicle that no longer
     * qualifies for export and clear the local AutoTrader references. The stock id comes from
     * the notification because the vehicle may not have been linked locally yet.
     */
    private function removeVehicleFromAutoTrader(Vehicle $vehicle): void
    {
        if (!Settings::get('auto-trader-hub-remove-unmatched-vehicles')) {
            return;
        }

        $vehicle->auto_trader_id = $this->input['stock_id'] ?? $vehicle->auto_trader_id;

        resolve(AutoTraderHub::class)
            ->setToken(Settings::get('auto-trader-hub-token'))
            ->removeVehicle($vehicle);

        RemoveAutoTraderReferences::dispatch($vehicle);
    }

    private function handleUpsertError(): void
    {
        $vehicle = Vehicle::query()
            ->where('id', $this->input['vehicle_id'])
            ->withTrashed()
            ->first();

        if (!$vehicle) {
            return;
        }

        $existing = $vehicle->autoTraderData()->first();
        $previousError = $existing?->at_export_error;
        $hasDerivative = !empty($existing?->derivative_id);

        if ($existing) {
            $existing->update(['at_export_error' => $this->input['error']]);
        } else {
            $vehicle->autoTraderData()->create(array_merge(
                ['at_export_error' => $this->input['error']],
                $this->defaultPublishFlags($vehicle)
            ));
        }

        if (
            $this->input['error'] === 'missing_derivative'
            && $previousError === null
            && !$hasDerivative
        ) {
            ApplyAutoTraderDerivativeMapping::dispatch($vehicle->id);
        }
    }

    private function defaultPublishFlags(Vehicle $vehicle): array
    {
        if (Settings::get('sales-channels-auto-trader-enabled') === false) {
            return [];
        }
        return [
            'publish_advert' => $vehicle->is_new
                ? (bool) Settings::get('auto-trader-connect-publish-new-cars')
                : (bool) Settings::get('auto-trader-connect-publish-used-cars'),
            'publish_profile' => $vehicle->is_new
                ? (bool) Settings::get('auto-trader-connect-profile-new-cars')
                : (bool) Settings::get('auto-trader-connect-profile-used-cars'),
        ];
    }

    private function setAutoTraderIdForVehicle(): void
    {
        $query = Vehicle::query()
            ->where('id', $this->input['vehicle_id'])
            ->withTrashed();

        if (array_key_exists('old_stock_id', $this->input)) {
            if (is_null($this->input['old_stock_id'])) {
                $query->whereNull('auto_trader_id');
            } else {
                $query->where('auto_trader_id', $this->input['old_stock_id']);
            }
        }

        $query->update([
            'auto_trader_id' => $this->input['stock_id'],
        ]);
        /** @var Vehicle|null $vehicle */
        $vehicle = Vehicle::query()->where('auto_trader_id', $this->input['stock_id'])->first();
        if ($vehicle?->autoTraderData()->exists()) {
            $data = $this->input['data'];
            $vehicle->autoTraderData()->update([
                'at_export_error' => null,
                'advert_publish_state' => $data['adverts']['retailAdverts']['autotraderAdvert']['status'] ?? null,
                'profile_publish_state' => $data['adverts']['retailAdverts']['profileAdvert']['status'] ?? null,
                'lifecycle_state' => $data['metadata']['lifecycleState'] ?? null,
            ]);
        }
        if ($vehicle) {
            Event::dispatch(new AutoTraderIdAssignedToVehicle($vehicle));
        }
    }

    private function assignUploadedImages(): void
    {
        if (isset($this->input['uploaded_images'])) {
            collect($this->input['uploaded_images'])
                ->each(function ($image) {
                    $media = Media::query()
                        ->where('id', $image['media_id'])
                        ->first();
                    if ($media) {
                        $media->update([
                            'image_provider' => $media->image_provider ?? 'auto-trader',
                            'auto_trader_id' => $image['auto_trader_id']
                        ]);
                    }
                });
        }
    }

    private function handleStockUpdate($importImages = true): void
    {
        if (new IntegrationRepository()->isEnabled('auto-trader') === false) {
            /** @var Vehicle|null $vehicle */
            $vehicle = Vehicle::query()->where('auto_trader_id', $this->input['stock_id'])->first();
            if ($vehicle?->autoTraderData()->exists()) {
                $data = $this->input['data'];
                $vehicle->autoTraderData()->update([
                    'at_export_error' => null,
                    'advert_publish_state' => $data['adverts']['retailAdverts']['autotraderAdvert']['status'] ?? null,
                    'profile_publish_state' => $data['adverts']['retailAdverts']['profileAdvert']['status'] ?? null,
                    'lifecycle_state' => $data['metadata']['lifecycleState'] ?? null,
                ]);
            }

            // AutoTrader is not a stock provider, we do not allow creating or updating vehicles
            return;
        }
        $vehicle_data = $this->input['data'];

        $this->assignDealershipFromAdvertiser($vehicle_data);

        if ($this->import->exists($vehicle_data, $this->input['vehicle_id'] ?? null)) {
            $this->import->update($vehicle_data, $this->input['vehicle_id'] ?? null, $importImages);
        } else {
            $this->import->add($vehicle_data, $importImages);
        }

        $this->apiNotification->processed = 1;
        $this->apiNotification->save();
    }

    /**
     * Resolve the local dealership from the notification's AutoTrader advertiser id and set it
     * on the importer, so created/updated vehicles are assigned to the right location. The
     * advertiser id is matched against the dealership's "Dealer ID within AutoTrader" setting
     * (data['auto-trader-location-id']).
     */
    private function assignDealershipFromAdvertiser(array $vehicle_data): void
    {
        $advertiserId = $this->input['advertiser']['advertiserId']
            ?? ($vehicle_data['advertiser']['advertiserId'] ?? null);

        if ($advertiserId === null) {
            return;
        }

        if (Settings::get('autotrader-make-based-assignment')) {
            // Co-located dealerships share the advertiser id. Pin the deterministic catchall as a
            // fallback; the importer refines to the make-specific sibling per vehicle.
            $matchedDealership = app(AutoTraderDealershipResolver::class)->resolve($advertiserId, null);
        } else {
            $matchedDealership = Dealership::query()->get()
                ->first(function (Dealership $dealership) use ($advertiserId) {
                    return ($dealership->data['auto-trader-location-id'] ?? null) === (string) $advertiserId;
                });
        }

        if ($matchedDealership) {
            $this->import->setDealershipId($matchedDealership->id);
        }
    }

    private function handleUnrecognizedAction($action): void
    {
        Log::warning('AutoTrader Hub notification - unrecognized action', [
            'action' => $action,
            'input' => $this->input,
            'tenant' => tenant('id'),
        ]);
    }

    private function handleImageOrdering(): void
    {
        if (!(Settings::get('auto-trader-upsert-sort-images', false) ?? false)) {
            return;
        }

        $orderedImageIds = collect($this->input['data']['media']['images'] ?? [])
            ->pluck('imageId')
            ->filter()
            ->values();

        if ($orderedImageIds->isEmpty()) {
            return;
        }


        $mediaMap = Media::query()
            ->whereIn('auto_trader_id', $orderedImageIds)
            ->get()
            ->keyBy('auto_trader_id');


        foreach ($orderedImageIds as $order => $autoTraderImageId) {
            $media = $mediaMap->get($autoTraderImageId);

            if (!$media) {
                continue;
            }

            $media->uses()
                ->where('owner_type', 'vehicle')
                ->where('owner_id', $this->input['vehicle_id'])
                ->update([
                    'order' => $order
                ]);
        }
    }
}
