<?php

namespace App\Imports;

use App\Events\NewVehicleImported;
use App\Events\VehicleUpdatedFromImport;
use App\Facades\Feature;
use App\Facades\Settings;
use App\Master\Models\CapVehicleAutoGrabId;
use App\TaxonomyMap;
use App\Traits\EnsuresVehicleAttribute;
use App\Traits\ImportChecksConditions;
use App\Traits\MapsTaxonomies;
use App\Traits\StockSyncTraits;
use App\Traits\VehicleRegistrationDateFinderTrait;
use App\VehicleType;
use Carbon\Carbon;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Mtc\ContentManager\Facades\Media as MediaFacade;
use Mtc\MercuryDataModels\Finance\FinanceServiceHelper;
use Mtc\MercuryDataModels\Media;
use Mtc\MercuryDataModels\MediaUse;
use Mtc\MercuryDataModels\Services\FinanceService;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleModel;

class AutoGrabToVehicleImport
{
    use DispatchesJobs;
    use MapsTaxonomies;
    use StockSyncTraits;
    use ImportChecksConditions;
    use EnsuresVehicleAttribute;
    use VehicleRegistrationDateFinderTrait;

    private array $vehicleAttributes;

    /** Cache of model_id => resolved vehicle type, reused across notifications in a drain run. */
    private array $modelVehicleTypeCache = [];

    /**
     * When rows come from the daily Parquet file feed rather than webhooks, the source is
     * already narrowed by AutoGrab's eligibility filter and carries no listing_sources
     * column, so the per-tenant listing-source filter must not be applied (it would reject
     * every row). Webhook processing leaves this false and keeps filtering as before.
     */
    private bool $bypassListingSourceFilter = false;

    public function fromFileFeed(bool $bypass = true): self
    {
        $this->bypassListingSourceFilter = $bypass;

        return $this;
    }

    public function exists(array $vehicle_data): bool
    {
        $vrm = $vehicle_data['rego'] ?? null;
        $vin = $vehicle_data['vin'] ?? null;
        $autoGrabId = $vehicle_data['id'] ?? null;

        $query = Vehicle::query()->withTrashed();

        if ($autoGrabId) {
            $exists = (clone $query)
                ->where('stock_provider', $this->getProviderName())
                ->where('uuid', $autoGrabId)
                ->exists();

            if ($exists) {
                return true;
            }
        }

        if ($vrm) {
            $exists = (clone $query)
                ->where('registration_number', $vrm)
                ->exists();

            if ($exists) {
                return true;
            }
        }

        if ($vin) {
            $exists = (clone $query)
                ->where('vin', $vin)
                ->exists();

            if ($exists) {
                return true;
            }
        }

        return false;
    }

    /**
     * Apply a full AutoGrab webhook payload (containing `lead` + `event`) to stock.
     *
     * Each webhook is a complete snapshot, so the latest payload per vehicle is the
     * authoritative state; processing the newest pending notification is sufficient.
     */
    public function process(array $input): void
    {
        // REPROCESS notifications carry no new state and are dropped on receipt;
        // guard here too in case one was captured before that rule applied.
        if (($input['event'] ?? null) === 'REPROCESS') {
            return;
        }

        // Resolve the vehicle once and branch on the result, rather than running the
        // existence lookup here and the identical lookup again inside update().
        $vehicle = $this->findExistingVehicle($input['lead']);

        if ($vehicle) {
            $this->update($input['lead'], $vehicle);
        } elseif (($input['event'] ?? null) !== 'DEACTIVATE') {
            $this->add($input['lead']);
        }
    }

    public function update(array $vehicle_data, ?Vehicle $vehicle = null): void
    {
        $vehicle ??= $this->findExistingVehicle($vehicle_data);

        if (!$vehicle) {
            $this->add($vehicle_data);
            return;
        }

        if ($this->shouldFilterByListingSource($vehicle_data)) {
            if (!$vehicle->trashed()) {
                $this->removeWithMedia($vehicle);
            }
            return;
        }

        // Check import conditions and make sure we remove vehicle if it matches skip conditions
        $normalizedData = $this->normalizeDataForConditionCheck($vehicle_data);
        if ($this->skipImporting($this->getProviderName(), $normalizedData)) {
            if (!$vehicle->trashed()) {
                $this->removeWithMedia($vehicle);
            }
            return;
        }

        if ($vehicle->trashed()) {
            if ($this->isVehicleDeleted($vehicle_data)) {
                return;
            }

            $vehicle->restore();
        }

        $vehicle->fill($this->mapDataToColumns($vehicle_data, $vehicle));

        $vehicle->is_published = $this->shouldBePublished(
            fn () => $vehicle->is_published,
            $this->getProviderName(),
            $vehicle
        );

        $vehicle->save();
        $this->syncAdditionalData($vehicle, $vehicle_data);

        Event::dispatch(new VehicleUpdatedFromImport($vehicle, $vehicle_data, $this->getProviderName()));
        $this->checkFinanceQuotes($vehicle);
    }

    public function add(array $vehicle_data): void
    {
        if ($this->isVehicleDeleted($vehicle_data)) {
            return;
        }

        $normalizedData = $this->normalizeDataForConditionCheck($vehicle_data);
        // Skip private seller vehicles
        if ($vehicle_data['seller_type'] === 'private' || in_array("writeoff", $vehicle_data['tag_ids'])) {
            return;
        }

        if ($this->shouldFilterByListingSource($vehicle_data)) {
            return;
        }

        if ($this->skipImporting($this->getProviderName(), $normalizedData)) {
            return;
        }

        $vehicle = new Vehicle();
        $vehicle->fill($this->mapDataToColumns($vehicle_data, $vehicle, false));

        $vehicle->is_published = $this->shouldBePublished(
            fn () => $vehicle->is_published,
            $this->getProviderName(),
            $vehicle,
        );
        $vehicle->save();

        $this->syncAdditionalData($vehicle, $vehicle_data);

        Event::dispatch(new NewVehicleImported($vehicle, $vehicle_data, $this->getProviderName()));
        $this->checkFinanceQuotes($vehicle, true);
    }

    protected function findExistingVehicle(array $vehicle_data): ?Vehicle
    {
        $vrm = $vehicle_data['rego'] ?? null;
        $vin = $vehicle_data['vin'] ?? null;
        $autoGrabId = $vehicle_data['id'] ?? null;

        if ($autoGrabId) {
            $vehicle = Vehicle::query()
                ->withTrashed()
                ->where('stock_provider', $this->getProviderName())
                ->where('uuid', $autoGrabId)
                ->first();

            if ($vehicle) {
                return $vehicle;
            }
        }

        if ($vrm) {
            $vehicle = Vehicle::query()
                ->withTrashed()
                ->where('registration_number', $vrm)
                ->first();

            if ($vehicle) {
                return $vehicle;
            }
        }

        if ($vin) {
            $vehicle = Vehicle::query()
                ->withTrashed()
                ->where('vin', $vin)
                ->first();

            if ($vehicle) {
                return $vehicle;
            }
        }

        return null;
    }

    protected function mapDataToColumns(array $vehicle_data, Vehicle $vehicle, bool $existing_vehicle = true): array
    {
        $make_id = $this->getMappedTaxonomy(TaxonomyMap::MAKE, $vehicle_data['make'] ?? null, $vehicle_data);
        $model_id = $this->getMappedTaxonomy(
            TaxonomyMap::MODEL,
            $vehicle_data['model'] ?? null,
            $vehicle_data,
            $make_id
        );

        $data = array_filter([
            'was_recently_synced' => true,
            'type' => $this->getVehicleType($vehicle_data, $model_id),
            'stock_provider' => $vehicle->stock_provider ?? $this->getProviderName(),
            'uuid' => $vehicle_data['id'] ?? $vehicle->uuid,
            'title' => $vehicle_data['vehicle_title'] ?? '',
            'registration_number' => $vehicle_data['rego'] ?? null,
            'vin' => $vehicle_data['vin'] ?? null,
            'make_id' => $make_id,
            'model_id' => $model_id,
            'trim' => $vehicle_data['badge'] ?? null,
            'derivative' => $vehicle_data['series'] ?? null,
            'colour' => $vehicle_data['color'] ?? null,
            'fuel_type_id' => $this->getMappedTaxonomy(
                TaxonomyMap::FUEL_TYPE,
                $vehicle_data['fuel_type'] ?? null,
                $vehicle_data
            ),
            'body_style_id' => $this->getMappedTaxonomy(
                TaxonomyMap::BODY_STYLE,
                $vehicle_data['body_type'] ?? null,
                $vehicle_data
            ),
            'transmission_id' => $this->getMappedTaxonomy(
                TaxonomyMap::TRANSMISSION,
                $vehicle_data['transmission_type'] ?? null,
                $vehicle_data
            ),
            'engine_size_cc' => $vehicle_data['capacity_cc'] ?? null,
            'manufacture_year' => $vehicle_data['year'] ?? $vehicle_data['release_year'] ?? null,
            'price' => $vehicle_data['price'] ?? null,
            'rrp_price' => $vehicle_data['vehicle_rrp'] ?? null,
            // AutoGrab sends mileage through using kms, which may seem counter-intuitive
            'odometer_mi' => $vehicle_data['kms'] ?? null,
            'odometer_km' => !empty($vehicle_data['kms'])
                ? new Vehicle()->milesToKm($vehicle_data['kms'])
                : null,
            'seats' => $vehicle_data['num_seats'] ?? null,
            'door_count' => $vehicle_data['num_doors'] ?? null,
            'bhp' => $this->kwToBhp($vehicle_data['power_kw'] ?? null),
            'co2' => null,
            'is_new' => $this->isNewVehicle($vehicle_data),
            'lat' => $vehicle_data['lat'] ?? null,
            'lng' => $vehicle_data['lng'] ?? null,
            'data' => $vehicle_data,
            'first_registration_date' => $this->getRegistrationDateFromRegistrationNumber($vehicle_data['rego']),
        ], fn ($value) => !is_null($value));

        $capData = $this->lookupCapData($vehicle_data);
        if ($capData) {
            $data['cap_id'] = $capData['cap_id'];
            $data['cap_code'] = $capData['cap_code'];
        }

        $data['deleted_at'] = $this->isVehicleDeleted($vehicle_data) ? Carbon::now() : null;

        if (!$existing_vehicle) {
            $data['is_published'] = true;
        }

        if (empty($data['first_registration_date'])) {
            $data['is_published'] = false;
        }

        // Set description from listing if available
        $description = $this->getDescriptionFromListings($vehicle_data);
        if ($description) {
            $data['description'] = $description;
        }

        return $data;
    }

    protected function syncAdditionalData(Vehicle $vehicle, array $vehicle_data): void
    {
        $imageUrls = $this->collectImages($vehicle_data);
        if (!empty($imageUrls)) {
            $this->syncImages($vehicle, $imageUrls);
        }

        $this->syncVehicleAttributes($vehicle, $vehicle_data);
        $this->storeUnmappedTaxonomy($vehicle);
    }

    protected function syncVehicleAttributes(Vehicle $vehicle, array $vehicle_data): void
    {
        $this->retrieveRequiredVehicleAttributes();

        $this->setVehicleAttributes(
            [
                'contact_name' => $vehicle_data['contact_name'] ?? null,
                'postcode' => $vehicle_data['postcode'] ?? null,
            ],
            $vehicle
        );
    }

    protected function retrieveRequiredVehicleAttributes(): void
    {
        // The attribute definitions are global and stable, so cache them briefly to
        // avoid two firstOrCreate reads on every notification during a drain.
        $this->vehicleAttributes = Cache::remember(
            'autograb-required-vehicle-attributes:' . (tenant('id') ?? 'none'),
            now()->addMinutes(5),
            fn () => [
                'contact_name' => $this->getVehicleAttribute('Contact Name', 'text')->toArray(),
                'postcode' => $this->getVehicleAttribute('Postcode', 'text')->toArray(),
            ]
        );
    }

    protected function collectImages(array $vehicle_data): array
    {
        $images = [];

        if (!empty($vehicle_data['cover_image_url'])) {
            $images[] = $vehicle_data['cover_image_url'];
        }

        for ($i = 2; $i <= 5; $i++) {
            $key = "image_url_{$i}";
            if (!empty($vehicle_data[$key])) {
                $images[] = $vehicle_data[$key];
            }
        }

        return $images;
    }

    protected function syncImages(Vehicle $vehicle, array $imageUrls): void
    {
        if (empty($imageUrls)) {
            return;
        }

        $desiredSrcs = collect($imageUrls)->unique()->values();

        // Images already linked to this vehicle, keyed by src so we can diff against
        // the incoming snapshot. AutoGrab webhooks are full snapshots, so in steady
        // state the desired and current sets match and there is nothing to do.
        $currentUses = $vehicle->mediaUses()
            ->with('media:id,src')
            ->get()
            ->filter(fn (MediaUse $use) => $use->media !== null);

        $currentSrcs = $currentUses->map(fn (MediaUse $use) => $use->media->src);

        $newSrcs = $desiredSrcs->reject(fn ($src) => $currentSrcs->contains($src))->values();
        $removedUses = $currentUses->reject(fn (MediaUse $use) => $desiredSrcs->contains($use->media->src));

        // Re-snapshot with an identical image set — skip the media writes entirely.
        if ($newSrcs->isEmpty() && $removedUses->isEmpty()) {
            return;
        }

        if ($newSrcs->isNotEmpty()) {
            $this->addImages($vehicle, $newSrcs);
        }

        if ($removedUses->isNotEmpty()) {
            $this->removeImageUses($removedUses);
        }
    }

    /**
     * Create media records for the new image URLs and link them to the vehicle.
     */
    private function addImages(Vehicle $vehicle, Collection $newSrcs): void
    {
        $records = $newSrcs->map(fn ($src) => [
            'src' => $src,
            'image_provider' => $this->getProviderName(),
            'type' => 'image',
            'external' => true,
            'folder_id' => null,
            'path' => '',
            'upload_date' => Carbon::now()->format('Y-m'),
        ]);

        Media::query()->upsert($records->toArray(), ['src', 'image_provider']);

        $mediaIds = Media::query()
            ->whereIn('src', $newSrcs)
            ->where('image_provider', $this->getProviderName())
            ->pluck('id')
            ->toArray();

        // Link only the newly added media; existing uses already carry primary = true.
        MediaFacade::setUsesForModel($mediaIds, $vehicle, ['primary' => true], false);
    }

    /**
     * Detach media uses for images dropped from the snapshot and delete any media
     * records left orphaned. Uses batch queries rather than a per-use existence check.
     *
     * @param Collection<int, MediaUse> $removedUses
     */
    private function removeImageUses(Collection $removedUses): void
    {
        $removedMediaIds = $removedUses->pluck('media_id')->unique()->values();

        MediaUse::query()->whereIn('id', $removedUses->pluck('id'))->delete();

        // Media still referenced by another use must be kept; the rest are orphans.
        $stillReferenced = MediaUse::query()
            ->whereIn('media_id', $removedMediaIds)
            ->pluck('media_id')
            ->unique();

        $orphanIds = $removedMediaIds->reject(fn ($id) => $stillReferenced->contains($id))->values();

        if ($orphanIds->isNotEmpty()) {
            Media::query()->whereIn('id', $orphanIds)->delete();
        }
    }

    protected function getDescriptionFromListings(array $vehicle_data): ?string
    {
        $listings = $vehicle_data['listings'] ?? [];
        if (empty($listings)) {
            return null;
        }

        // Get the first listing description
        $description = $listings[0]['description'] ?? null;
        if ($description) {
            // Strip HTML tags for clean text
            return strip_tags((string) $description);
        }

        return null;
    }

    protected function isVehicleDeleted(array $vehicle_data): bool
    {
        return !empty($vehicle_data['removed_at']);
    }

    protected function isNewVehicle(array $vehicle_data): bool
    {
        return ($vehicle_data['kms'] ?? 0) < 1000;
    }

    protected function getVehicleType(array $vehicle_data, ?int $model_id = null): string
    {
        // First, try to get vehicle type from the vehicle model. The lookup is memoised
        // because getVehicleType() is called for the same model twice per notification
        // (mapDataToColumns + normalizeDataForConditionCheck) and the model->type mapping
        // is stable for the duration of a drain run.
        if ($model_id) {
            $type = $this->modelVehicleType($model_id);
            if ($type) {
                return $type;
            }
        }

        // Fall back to determining type from body_type
        $bodyType = strtolower($vehicle_data['body_type'] ?? '');

        if (str_contains($bodyType, 'van') || str_contains($bodyType, 'commercial')) {
            return VehicleType::LCV->value;
        }

        if (str_contains($bodyType, 'motorcycle') || str_contains($bodyType, 'bike')) {
            return VehicleType::MOTORCYCLE->value;
        }

        return VehicleType::CAR->value;
    }

    private function modelVehicleType(int $model_id): ?string
    {
        if (!array_key_exists($model_id, $this->modelVehicleTypeCache)) {
            $this->modelVehicleTypeCache[$model_id] = VehicleModel::find($model_id)?->type ?: null;
        }

        return $this->modelVehicleTypeCache[$model_id];
    }

    protected function kwToBhp(?int $kw): ?int
    {
        if ($kw === null) {
            return null;
        }
        return (int) round($kw * 1.34102);
    }

    public function getProviderName(): string
    {
        return 'autograb';
    }

    /**
     * Normalize AutoGrab data fields to Vehicle model field names for condition checks.
     * This allows import conditions to be configured using standard Vehicle field names.
     */
    protected function normalizeDataForConditionCheck(array $vehicle_data): array
    {
        $make_id = $this->getMappedTaxonomy(TaxonomyMap::MAKE, $vehicle_data['make'] ?? null, $vehicle_data);
        $model_id = $this->getMappedTaxonomy(
            TaxonomyMap::MODEL,
            $vehicle_data['model'] ?? null,
            $vehicle_data,
            $make_id
        );

        return [
            // Direct mappings from AutoGrab to Vehicle fields
            'registration_number' => $vehicle_data['rego'] ?? null,
            'vrm_condensed' => isset($vehicle_data['rego'])
                ? str_replace(' ', '', $vehicle_data['rego'])
                : null,
            'vin' => $vehicle_data['vin'] ?? null,
            'uuid' => $vehicle_data['id'] ?? null,
            'title' => $vehicle_data['vehicle_title'] ?? null,
            'make' => $vehicle_data['make'] ?? null,
            'model' => $vehicle_data['model'] ?? null,
            'trim' => $vehicle_data['badge'] ?? null,
            'derivative' => $vehicle_data['series'] ?? null,
            'colour' => $vehicle_data['color'] ?? null,
            'fuel_type' => $vehicle_data['fuel_type'] ?? null,
            'body_style' => $vehicle_data['body_type'] ?? null,
            'transmission' => $vehicle_data['transmission_type'] ?? null,
            'engine_size_cc' => $vehicle_data['capacity_cc'] ?? null,
            'manufacture_year' => $vehicle_data['year'] ?? $vehicle_data['release_year'] ?? null,
            'price' => $vehicle_data['price'] ?? null,
            'rrp_price' => $vehicle_data['vehicle_rrp'] ?? null,
            // AutoGrab sends "local mileage value" as kms field, meaning UK gets miles in kms field
            'odometer_mi' => $vehicle_data['kms'] ?? null,
            'odometer_km' => !empty($vehicle_data['kms'])
                ? new Vehicle()->milesToKm($vehicle_data['kms'])
                : null,
            'seats' => $vehicle_data['num_seats'] ?? null,
            'door_count' => $vehicle_data['num_doors'] ?? null,
            'bhp' => $this->kwToBhp($vehicle_data['power_kw'] ?? null),
            'is_new' => $this->isNewVehicle($vehicle_data),
            'type' => $this->getVehicleType($vehicle_data, $model_id),
            // Custom attributes, exposed to import conditions as "attribute.{slug}" fields
            // (slugs match the attributes synced in syncVehicleAttributes()).
            'attribute.contact_name' => $vehicle_data['contact_name'] ?? null,
            'attribute.postcode' => $vehicle_data['postcode'] ?? null,
        ];
    }

    protected function shouldFilterByListingSource(array $vehicle_data): bool
    {
        if ($this->bypassListingSourceFilter) {
            return false;
        }

        $allowed = Settings::get('stock-autograb-listing-sources', '');
        if (empty($allowed)) {
            return false;
        }

        $allowedSources = array_map('trim', explode(',', $allowed));
        $vehicleSources = $vehicle_data['listing_sources'] ?? [];

        return empty(array_intersect($allowedSources, $vehicleSources));
    }

    protected function lookupCapData(array $vehicle_data): ?array
    {
        if (!Feature::isEnabled('cap-database')) {
            return null;
        }

        $vehicleId = $vehicle_data['vehicle_id'] ?? null;
        if (!$vehicleId) {
            return null;
        }

        $mapping = CapVehicleAutoGrabId::query()->where('autograb_vehicle_id', $vehicleId)->first();
        if (!$mapping) {
            return null;
        }

        $capVehicle = $mapping->capVehicle;
        if (!$capVehicle || !$capVehicle->cap_id) {
            return null;
        }

        return [
            'cap_id' => $capVehicle->cap_id,
            'cap_code' => $capVehicle->cap_code,
        ];
    }

    protected function getDetailsForTaxonomyMap(?array $record): array
    {
        return [
            'tenant_id' => tenant('id'),
            'registration_number' => $record['rego'] ?? null,
            'make' => $record['make'] ?? null,
            'model' => $record['model'] ?? null,
        ];
    }

    private function checkFinanceQuotes(Vehicle $vehicle, bool $create = false): void
    {
        if (empty($vehicle->cap_id) || !FinanceServiceHelper::hasEnabledProvider()) {
            return;
        }

        if (!$create || $vehicle->financeExamples()->exists()) {
            return;
        }

        new FinanceService()->request($vehicle, [], true);
    }

    /**
     * Daily Reconcile convergence: deactivate active AutoGrab vehicles that this run neither
     * synced nor queued for sync (was_recently_synced = false AND pending_stock_sync IS NULL),
     * i.e. those absent from the authoritative snapshot because they were delisted or fell out
     * of the eligibility filter.
     *
     * This mirrors the Motors provider's removeOld(): the caller resets both flags at the
     * start of the run and marks every in-feed vehicle pending_stock_sync = 1 before its batch
     * job is dispatched, so a vehicle still queued for an async batch is protected here even
     * though its batch has not run yet. Removal is per-model (soft delete + orphaned-media
     * cleanup), matching the webhook DEACTIVATE path rather than a bulk builder delete.
     *
     * @return int Number of vehicles deactivated.
     */
    public function deactivateNotRecentlySynced(): int
    {
        $count = 0;

        Vehicle::query()
            ->where('stock_provider', $this->getProviderName())
            ->where('was_recently_synced', false)
            ->whereNull('pending_stock_sync')
            ->chunkById(200, function ($vehicles) use (&$count) {
                foreach ($vehicles as $vehicle) {
                    $this->removeWithMedia($vehicle);
                    $count++;
                }
            });

        return $count;
    }
}
