<?php

namespace App\Imports\Traits;

use App\Events\NewVehicleImported;
use App\Events\VehicleUpdatedFromImport;
use App\Facades\Settings;
use App\Jobs\ImportImagesFromUrlList;
use App\Models\ImportMap;
use App\Models\ImportMapField;
use App\TaxonomyMap;
use App\Traits\EnsuresVehicleAttribute;
use App\Traits\ImportChecksConditions;
use App\Traits\StockSyncTraits;
use App\Traits\TracksImportStats;
use App\Traits\VehicleRegistrationDateFinderTrait;
use Mtc\MercuryDataModels\VehicleType;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Mtc\ContentManager\Facades\Media;
use Mtc\ContentManager\Facades\Media as MediaFacade;
use Mtc\MercuryDataModels\BodyStyleType;
use Mtc\MercuryDataModels\Country;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\DrivetrainType;
use Mtc\MercuryDataModels\FuelType;
use Mtc\MercuryDataModels\TransmissionType;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleAttribute;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;
use Mtc\MercuryDataModels\VehicleSpecType;
use Symfony\Component\Finder\SplFileInfo;

trait VehicleImportFlow
{
    use EnsuresVehicleAttribute;
    use ImportChecksConditions;
    use StockSyncTraits;
    use TracksImportStats;
    use VehicleRegistrationDateFinderTrait;

    protected $subFieldDelimiter = '|';
    protected static array $allowedFieldsToUpdate = [];

    /** Tracks which maps have already had their match-key misconfiguration warned during this request. */
    protected static array $warnedMatchKeyMaps = [];

    protected array $vehicleAttributes;
    protected bool $stock_sync = false;
    protected ?string $archive_folder = null;

    /** IDs of vehicles created or updated from the imported file, used for missing-vehicle removal. */
    protected array $processed_vehicle_ids = [];

    public function setAdditionalTasks(array $tasks): self
    {
        $this->additional_tasks = $tasks;
        return $this;
    }

    public function setImportMap(ImportMap $importMap, bool $stockSync = false): self
    {
        $this->import_map = $importMap;
        $this->stock_sync = $stockSync;
        if (!empty($this->import_map->data['sub_field_delimiter'])) {
            $this->subFieldDelimiter = $this->import_map->data['sub_field_delimiter'];
        }
        $this->retrieveRequiredVehicleAttributes();
        return $this;
    }

    public function setArchiveFolder(string $archive_folder): self
    {
        $this->archive_folder = $archive_folder;
        return $this;
    }

    protected function retrieveRequiredVehicleAttributes(): void
    {
        $model = $this->import_map->data['target_model'] ?? 'vehicle';
        $this->vehicleAttributes = VehicleAttribute::query()
            ->where('model', $model)
            ->get()
            ->mapWithKeys(fn ($attribute) => [
                $attribute->slug => $attribute->toArray(),
            ])->toArray();
    }

    private function processRow($row): ?Vehicle
    {
        if (empty($row)) {
            return null;
        }

        $skip = $this->skipImporting('import-map', $row, isset($this->import_map) ? $this->import_map->id : null);
        if ($skip) {
            return null;
        }

        $search_params = $this->importRowSearchParams($row);

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

        /** @var Vehicle|null $model */
        $model = empty($this->import_map->data['update_only'])
            ? Vehicle::query()->firstOrNew($search_params)
            : Vehicle::query()->where($search_params)->first();

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

        $originalPrice = $model->price;
        $model->fill($this->getFillableData($row, $model));

        $model->is_published = $this->shouldBePublished(
            fn () => $model->is_published ?: false,
            'import-map',
            $model,
            isset($this->import_map) ? $this->import_map->id : null,
        );
        if ($model->isDirty()) {
            $model->save();
            $this->priceChangeEvents($model, 'import-map', $originalPrice);
        }

        if ($model->id) {
            $this->processed_vehicle_ids[] = $model->id;
        }

        $this->syncImages($model, $row['images'] ?? '');
        $this->syncFeatures($model, $row['features'] ?? '');
        $this->syncStandardEquipment($model, $row['specs'] ?? '');
        $this->syncAttributes($model, $row);
        $this->syncVehicleAutotraderData($model, $row);

        if ($model->wasRecentlyCreated) {
            $this->incrementCreated();
            Event::dispatch(new NewVehicleImported($model, $row, $this->getProviderName()));
        } else {
            $this->incrementUpdated();
            Event::dispatch(new VehicleUpdatedFromImport($model, $row, $this->getProviderName()));
        }

        return $model;
    }

    private function syncVehicleAutotraderData(Vehicle $vehicle, array $row): void
    {
        if (!Settings::get('sales-channels-auto-trader-enabled')) {
            return;
        }

        $combined = $this->castToBoolean($row['autotrader_enabled'] ?? '');

        $vehicle->autoTraderData()->updateOrCreate([
            'vehicle_id' => $vehicle->id
        ], [
            'publish_advert' => array_key_exists('publish_advert', $row)
                ? $this->castToBoolean($row['publish_advert'])
                : $combined,
            'publish_profile' => array_key_exists('publish_profile', $row)
                ? $this->castToBoolean($row['publish_profile'])
                : $combined,
        ]);
    }

    private function syncAttributes(Vehicle $vehicle, array $row): void
    {
        $attributes = collect($row)
            ->filter(fn ($value, $key) => stripos((string) $key, 'attribute.') !== false)
            ->mapWithKeys(function ($value, $key) use ($row) {
                $attribute_key = str_replace('attribute.', '', $key);

                return [
                    $attribute_key => $this->castValue($key, $value, $row),
                ];
            })
            ->toArray();

        $presentAttributeIds = collect($attributes)
            ->keys()
            ->map(fn ($slug) => $this->vehicleAttributes[$slug]['id'] ?? null)
            ->filter()
            ->values()
            ->toArray();

        $deleteQuery = $vehicle->attributeValues();
        if (!empty($presentAttributeIds)) {
            $deleteQuery->whereNotIn('attribute_id', $presentAttributeIds);
        }
        $deleteQuery->delete();

        $this->setVehicleAttributes($attributes, $vehicle);
    }

    private function importRowSearchParams($row): ?array
    {
        if (isset($this->import_map)) {
            // only for import maps that uses a match_key. E.g http_json
            $matchKeyParams = $this->resolveSearchParamsFromMatchKeys($row);
            if ($matchKeyParams !== false) {
                return $matchKeyParams;
            }
        }

        if (!empty($row['slug'])) {
            $search_params = [
                'slug' => $row['slug'],
            ];
        } elseif (empty($row['uuid'])) {
            if (empty($row['registration_number'])) {
                return null;
            }
            $row['uuid'] = 'import-' . $row['registration_number'];
        }

        if (empty($search_params)) {
            $search_params = [
                'uuid' => $row['uuid'],
            ];
            if (!empty($row['stock_provider'])) {
                $search_params['stock_provider'] = $row['stock_provider'];
            }
        }

        return $search_params;
    }

    /**
     * Resolve search params from fields flagged is_match_key.
     *
     * Returns:
     *  - array<string, mixed>  — params to use for the vehicle lookup
     *  - null                  — match keys are configured but none had a value in this row; skip the row
     *  - false                 — no match keys configured on the map; caller should fall back to the legacy chain
     *
     * @return array<string, mixed>|null|false
     */
    private function resolveSearchParamsFromMatchKeys(array $row): array|null|false
    {
        $flaggedFields = $this->import_map->fields
            ->filter(fn (ImportMapField $field) => !empty($field->is_match_key));

        $this->warnOnceForUnmappedMatchKeys($flaggedFields);

        $matchKeyFields = $flaggedFields
            ->pluck('on_model')
            ->filter()
            ->values();

        if ($matchKeyFields->isEmpty()) {
            return false;
        }

        foreach ($matchKeyFields as $field) {
            if (!empty($row[$field])) {
                return [$field => $row[$field]];
            }
        }

        return null;
    }

    /**
     * Log a one-time warning per import map when fields are flagged as match keys
     * but have no destination column (on_model) — they'd be silently dropped otherwise.
     */
    private function warnOnceForUnmappedMatchKeys(Collection $flaggedFields): void
    {
        if (isset(self::$warnedMatchKeyMaps[$this->import_map->id])) {
            return;
        }
        self::$warnedMatchKeyMaps[$this->import_map->id] = true;

        $unmappedFlagged = $flaggedFields
            ->filter(fn (ImportMapField $field) => empty($field->on_model));

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

        Log::warning('Import map has match-key fields with no destination column — they will be ignored.', [
            'import_map_id' => $this->import_map->id,
            'fields' => $unmappedFlagged->pluck('on_file')->values()->all(),
        ]);
    }

    private function syncImages(Vehicle $vehicle, string $images): void
    {
        $provider = isset($this->import_map) && $this->stock_sync ? 'import-map-' . $this->import_map->id : null;
        $imageList = collect(explode($this->subFieldDelimiter, $images))
            ->filter();

        // Only when doing system default, import maps rely on source_filename field
        if (is_null($provider)) {
            $imageList = $imageList->reject(fn (string $image) => $this->imageAlreadyHostedOnServer($image));
        }

        if ($imageList->isEmpty() && $provider !== null) {
            $this->removeProviderImages($vehicle, $provider);
            return;
        }

        if ($imageList->isNotEmpty() && $this->imageChecksumMismatch($imageList->toArray(), $vehicle)) {
            if (stripos($imageList->first(), 'http') === false) {
                $this->processImageFiles($imageList->toArray());
                $this->syncImagesFromFile($vehicle, $imageList->toArray());
            } else {
                $this->dispatch(new ImportImagesFromUrlList(
                    $imageList,
                    $vehicle,
                    false,
                    $provider,
                    true
                ));
            }
        }
    }

    private function removeProviderImages(Vehicle $vehicle, string $provider): void
    {
        $vehicle->mediaUses()->whereHas('media', fn ($q) => $q->where('image_provider', $provider))->delete();

        $vehicle[$vehicle->imageChecksumAttribute()] = null;
        $vehicle->save();

        return;
    }

    private function syncFeatures(Vehicle $vehicle, string $features): void
    {
        collect(explode($this->subFieldDelimiter, $features))
            ->map(fn ($feature) => trim($feature))
            ->filter()
            ->each(fn ($feature) => $vehicle->features()->updateOrCreate(['name' => $feature]));
    }

    private function syncStandardEquipment(Vehicle $vehicle, string $equipment): void
    {
        collect(explode($this->subFieldDelimiter, $equipment))
            ->map(fn ($description) => trim($description))
            ->filter()
            ->each(fn ($description) => $vehicle->equipment()->updateOrCreate(
                ['type' => VehicleSpecType::STANDARD_EQUIPMENT->value, 'description' => $description]
            ));
    }

    private function imageAlreadyHostedOnServer($image): bool
    {
        try {
            return Media::assetExists($image);
        } catch (Exception) {
            return false;
        }
    }

    private function getFillableData(array $vehicle, $model): array
    {
        if (empty(self::$vehicleFillable)) {
            self::$vehicleFillable = new Vehicle()->getFillable();
        }

        if ($model->exists) {
            $tenantId = tenant('id');

            if (empty(self::$allowedFieldsToUpdate[$tenantId])) {
                if (isset($this->import_map)) {
                    self::$allowedFieldsToUpdate[$tenantId] = $this->import_map->fields
                        ->filter(fn (ImportMapField $field) => $field->data['save_on_update'] ?? true)
                        ->pluck('on_model')
                        ->filter()
                        ->toArray();
                } else {
                    self::$allowedFieldsToUpdate[$tenantId] = self::$vehicleFillable;
                }
            }

            $vehicle =  array_intersect_key($vehicle, array_flip(self::$allowedFieldsToUpdate[$tenantId]));
        }

        $vehicle['was_recently_synced'] = true;
        if (!$model->exists && empty($vehicle['stock_provider'])) {
            $vehicle['stock_provider'] = $this->getProviderName();
        }

        $data = collect($vehicle)
            ->mapWithKeys(fn ($value, $field) => [
                $this->getMappedField($field) => $this->getMappedValue($field, $value, $vehicle)
            ])
            ->only(self::$vehicleFillable)
            ->toArray();

        return $this->ensureBothOdometerValues($data);
    }

    private function ensureBothOdometerValues(array $data): array
    {
        $converter = new Vehicle();

        if (!empty($data['odometer_km']) && empty($data['odometer_mi'])) {
            $data['odometer_mi'] = $converter->kmToMiles((int) $data['odometer_km']);
        } elseif (!empty($data['odometer_mi']) && empty($data['odometer_km'])) {
            $data['odometer_km'] = $converter->milesToKm((int) $data['odometer_mi']);
        }

        return $data;
    }

    private function getMappedField($field): string
    {
        return match ($field) {
            'make' => 'make_id',
            'model' => 'model_id',
            'dealership' => 'dealership_id',
            'fuel_type' => 'fuel_type_id',
            'body_style' => 'body_style_id',
            'drivetrain' => 'drivetrain_id',
            'transmission' => 'transmission_id',
            default => $field,
        };
    }

    private function getMappedValue($field, $value, array $vehicle)
    {
        return match ($field) {
            'make' => $value && !empty($this->makes[$value])
                ? $this->makes[$value]
                : $this->getMappedTaxonomy(
                    TaxonomyMap::MAKE,
                    $value,
                    $vehicle
                ),
            'model' => !empty($value)
                ? $this->getModelValue($value, $vehicle)
                : null,
            'dealership' => $value && !empty($this->dealerships[$value])
                ? $this->dealerships[$value]
                : $this->getDealershipId($value),
            'fuel_type' => $value && !empty($this->fuel_types[$value])
                ? $this->fuel_types[$value]
                : $this->getMappedTaxonomy(
                    TaxonomyMap::FUEL_TYPE,
                    $value,
                    $vehicle
                ),
            'body_style' => $value && !empty($this->body_styles[$value])
                ? $this->body_styles[$value]
                : $this->getMappedTaxonomy(
                    TaxonomyMap::BODY_STYLE,
                    $value,
                    $vehicle
                ),
            'drivetrain' => $value && !empty($this->drivetrains[$value])
                ? $this->drivetrains[$value]
                : $this->getMappedTaxonomy(
                    TaxonomyMap::DRIVETRAIN,
                    $value,
                    $vehicle
                ),
            'transmission' => $value && !empty($this->transmissions[$value])
                ? $this->transmissions[$value]
                : $this->getMappedTaxonomy(
                    TaxonomyMap::TRANSMISSION,
                    $value,
                    $vehicle
                ),
            'featured' => $value ?? 0,
            default => $this->castValue($field, $value, $vehicle),
        };
    }

    private function getModelValue($value, array $vehicle)
    {
        $cast_type = $this->import_map->fields
            ->filter(fn ($entry) => $entry->on_model === 'model')
            ->first()
            ?->data['cast_type'] ?? null;

        if ($cast_type === 'model_lookup') {
            $this->castToModelLookup($value, $vehicle['make']);
        }

        return $this->getMappedTaxonomy(
            TaxonomyMap::MODEL,
            $value,
            $vehicle,
            $this->getMappedTaxonomy(
                TaxonomyMap::MAKE,
                $vehicle['make'],
                $vehicle
            )
        );
    }

    private function castValue(string $field, $value, array $vehicle): mixed
    {
        if (empty($this->import_map)) {
            return $value;
        }
        $fieldConfig = $this->import_map->fields
            ->filter(fn ($entry) => $entry->on_model === $field)
            ->first();

        $castType = $fieldConfig?->data['cast_type'] ?? null;

        return match ($castType) {
            'bool' => $this->castToBoolean($value),
            'number' => $this->parseNumericValue($value),
            'datetime' => $this->castToDateTime($value),
            'year_from_date' => $this->castToYearFromDate($value),
            'vehicle_type' => $this->castToVehicleType($value, $fieldConfig?->data['vehicle_type_mappings'] ?? []),
            'date_from_vrm' => $value,
            default => $value,
        };
    }

    private function castToBoolean($value): bool
    {
        return match (strtolower((string) $value)) {
            '1', 'yes', 'true', 'y', 'on', 'new' => true,
            default => false,
        };
    }

    private function castToDateTime($value): ?Carbon
    {
        try {
            $date = $this->parseDateValue($value);

            return $date?->gt(Carbon::create(1950)) ? $date : null;
        } catch (\Exception $exception) {
            return null;
        }
    }

    private function castToYearFromDate($value): ?int
    {
        try {
            $date = $this->parseDateValue($value);

            return $date?->year > 1950 ? $date->year : null;
        } catch (\Exception $exception) {
            Log::error($exception->getMessage());
            return null;
        }
    }

    private function castToDateFromVrm(array $row): ?string
    {
        $regField = $this->import_map->fields
            ->firstWhere('on_model', 'registration_number');

        if (!$regField) {
            return null;
        }

        $vrm = $row[$regField->on_file] ?? null;

        return $this->getRegistrationDateFromRegistrationNumber($vrm);
    }

    private function parseDateValue($value): ?Carbon
    {
        $dateFormat = $this->import_map->data['date_format'] ?? null;

        if ($dateFormat === 'excel') {
            return $this->parseExcelDate($value);
        }

        if (!empty($dateFormat)) {
            return Carbon::createFromFormat($dateFormat, $value);
        }

        return Carbon::parse($value);
    }

    private function parseExcelDate($value): ?Carbon
    {
        if (!is_numeric($value)) {
            return null;
        }

        // Excel serial date: days since December 30, 1899
        // Excel incorrectly treats 1900 as a leap year, so dates after Feb 28, 1900 are off by 1
        $excelEpoch = Carbon::create(1899, 12, 30);

        return $excelEpoch->addDays((int) $value);
    }

    private function resolveVehicleType($value, array $row): string
    {
        $vehicleTypeFieldConfig = $this->import_map->fields
            ->first(fn (ImportMapField $field) => $field->on_model === 'type');

        // if we are dealing with motorhomes, check the body type to determine motorhome or campervan
        if ($vehicleTypeFieldConfig?->default_value === VehicleType::MOTORHOME->value) {
            $bodyStyleField = $this->import_map->fields
                ->first(fn (ImportMapField $field) => $field->on_model === 'body_style_id');

            $rawBodyType = $bodyStyleField ? strtolower(trim($row[$bodyStyleField->on_file] ?? '')) : '';
            $isCampervan = str_contains($rawBodyType, 'camper') && str_contains($rawBodyType, 'van');

            return $isCampervan ? VehicleType::CAMPERVAN->value : VehicleType::MOTORHOME->value;
        }

        return $this->castToVehicleType($value, $vehicleTypeFieldConfig?->data['vehicle_type_mappings'] ?? []);
    }

    private function castToVehicleType($value, array $mappings): string
    {
        $normalizedValue = strtolower(trim($value ?? ''));

        foreach ($mappings as $mapping) {
            if (strtolower(trim($mapping['from'] ?? '')) === $normalizedValue) {
                $mapped = $mapping['to'] ?? '';

                return in_array($mapped, VehicleType::values(), true)
                    ? $mapped
                    : VehicleType::CAR->value;
            }
        }

        // Fallback to default matching if no custom mapping found
        return match ($normalizedValue) {
            'lcv' => VehicleType::LCV->value,
            'motorcycle' => VehicleType::MOTORCYCLE->value,
            'campervan', 'camper van' => VehicleType::CAMPERVAN->value,
            'motorhome' => VehicleType::MOTORHOME->value,
            default => VehicleType::CAR->value,
        };
    }

    private function castToModelFromTitle(string $value, array $row): ?int
    {
        $makeField = $this->import_map->fields
            ->first(fn (ImportMapField $field) => $field->on_model === 'make_id');
        $makeName = $row[$makeField->on_file] ?? null;

        if (!$makeName) {
            return null;
        }

        $makeId = $this->makes[$makeName] ?? $this->getMappedTaxonomy(TaxonomyMap::MAKE, $makeName, $row);

        if (!$makeId) {
            return null;
        }

        $makeNamesToStrip = $this->getMakeNameVariants($makeName, $makeId);
        $modelTerms = $this->getModelTerms($makeId);

        foreach ($makeNamesToStrip as $nameToStrip) {
            $remaining = $this->stripWordPrefix($nameToStrip, $value);

            if ($remaining === null) {
                continue;
            }

            foreach ($modelTerms as $term => $modelId) {
                if (Str::startsWith(strtolower($remaining), strtolower($term))) {
                    return $modelId;
                }
            }
        }
        return null;
    }

    /**
     * Collect all known name variants for a make: the file value, DB name, and taxonomy map terms.
     * Sorted longest-first so the most specific name is stripped first.
     */
    private function getMakeNameVariants(string $makeName, int $makeId): Collection
    {
        $names = collect([$makeName]);

        $resolvedName = $this->makes->search($makeId);
        if ($resolvedName && strtolower($resolvedName) !== strtolower($makeName)) {
            $names->push($resolvedName);
        }

        $taxonomyTerms = TaxonomyMap::query()
            ->whereIn('provider', array_filter([
                $this->getTaxonomyProviderName(),
                $this->getLegacyTaxonomyProviderName()
            ]))
            ->whereIn('taxonomy_type', [TaxonomyMap::MAKE, 'master-make'])
            ->where('taxonomy_id', $makeId)
            ->pluck('term');

        return $names
            ->merge($taxonomyTerms)
            ->unique(fn ($name) => strtolower($name))
            ->sortByDesc(fn ($name) => strlen($name));
    }

    /**
     * Get all model terms to match against: DB model names and mapped taxonomy terms.
     * Sorted longest-first so the most specific term matches first.
     */
    private function getModelTerms(int $makeId): Collection
    {
        $dbModels = VehicleModel::query()
            ->ofMake($makeId)
            ->pluck('id', 'name');

        $taxonomyTerms = TaxonomyMap::query()
            ->whereIn('provider', array_filter([
                $this->getTaxonomyProviderName(),
                $this->getLegacyTaxonomyProviderName()
            ]))
            ->whereIn('taxonomy_type', [TaxonomyMap::MODEL, 'master-model'])
            ->where('parent_id', $makeId)
            ->whereNotNull('taxonomy_id')
            ->pluck('taxonomy_id', 'term');

        return $dbModels
            ->union($taxonomyTerms)
            ->sortByDesc(fn ($id, $term) => strlen($term));
    }

    /**
     * Strip a prefix from a value only if it appears as a whole word
     * (followed by a space or end of string). Returns the remaining text or null.
     */
    private function stripWordPrefix(string $prefix, string $value): ?string
    {
        $lowerValue = strtolower($value);
        $lowerPrefix = strtolower($prefix);

        if (!Str::startsWith($lowerValue, $lowerPrefix)) {
            return null;
        }

        $after = substr($lowerValue, strlen($lowerPrefix));

        if ($after !== '' && $after[0] !== ' ') {
            return null;
        }

        $remaining = trim($after);

        return $remaining !== '' ? $remaining : null;
    }

    private function loadRelationships(): void
    {
        $this->dealerships = Dealership::all();
        $this->makes = VehicleMake::all()->pluck('id', 'name');
        $this->models = VehicleModel::all()->pluck('id', 'name');
        $this->fuel_types = FuelType::all()->pluck('id', 'name');
        $this->body_styles = BodyStyleType::all()->pluck('id', 'name');
        $this->drivetrains = DrivetrainType::all()->pluck('id', 'name');
        $this->transmissions = TransmissionType::all()->pluck('id', 'name');
        $this->retrieveRequiredVehicleAttributes();
    }

    /**
     * Remove vehicles belonging to this import's provider that were not present in the imported
     * file. Skipped when the file produced no vehicles so a bad upload cannot wipe the stock.
     * Removal is per-model (soft delete + orphaned-media cleanup) so audits, observers and
     * search index updates fire, matching the stock provider convergence pattern.
     */
    public function removeMissingVehicles(): void
    {
        if (empty($this->processed_vehicle_ids)) {
            return;
        }

        Vehicle::query()
            ->where('stock_provider', $this->getProviderName())
            ->whereNotIn('id', $this->processed_vehicle_ids)
            ->chunkById(200, function (Collection $vehicles) {
                foreach ($vehicles as $vehicle) {
                    $this->removeWithMedia($vehicle);
                }
            });
    }

    protected function getProviderName(): string
    {
        if (empty($this->import_map)) {
            return $this->importType;
        }

        return 'import-map-' . $this->import_map->id;
    }

    private function getTaxonomyProviderName(): string
    {
        if (empty($this->import_map)) {
            return $this->importType;
        }

        return tenant('id') . '-import-map-' . $this->import_map->id;
    }

    private function getLegacyTaxonomyProviderName(): ?string
    {
        if (empty($this->import_map)) {
            return null;
        }

        return 'import-map-' . $this->import_map->id;
    }

    /**
     * Get the dealership id from stock location
     *
     * @param $locationId
     * @return string|null
     */
    private function getDealershipId($locationId): ?string
    {
        if ($this->dealershipMatchValue === 'id') {
            return $this->dealerships->firstWhere('id', (int) $locationId)?->id;
        }

        return $this->dealerships
            ->filter(fn (Dealership $dealership) => $dealership[$this->dealershipMatchValue] == $locationId
                || ($dealership->data[$this->dealershipMatchValue] ?? null) == $locationId)
            ->first()
            ?->id;
    }

    private function mapBasedOnImportMap(array $row): array
    {
        if (!isset($this->import_map)) {
            $this->import_map = ImportMap::query()->with('fields')->find($this->importType);
            if (!$this->import_map) {
                throw new Exception('Unrecognized import type: ' . $this->importType);
            }
        }

        if (!empty($this->import_map->data['dealership_field'])) {
            $this->dealershipMatchValue = $this->import_map->data['dealership_field'];
        }

        $data = $this->import_map->fields
            ->keyBy(fn (ImportMapField $field) => $field->on_model)
            ->filter(fn (ImportMapField $field, $key) => !empty($key))
            ->map(fn (ImportMapField $field) => $this->ensureFormattingForField(
                $field->on_model,
                $row[$field->on_file] ?? $this->decodeDefaultValue($field->default_value, $row),
                $row,
                $field->data['cast_type'] ?? null
            ))
            ->reject(fn ($value) => is_null($value))
            ->toArray();
        return $this->verifyRowRequiredFields($data);
    }

    protected function verifyRowRequiredFields(array $data): array
    {
        $required_fields = $this->import_map->fields
            ->filter(fn (ImportMapField $field) => !empty($field->data['required_to_import']))
            ->pluck('on_model');

        $has_missing_required_fields = $required_fields
            ->filter(fn (string $field) => empty($data[$field]) || $this->rowFailsValidation($field, $data))
            ->isNotEmpty();

        return $has_missing_required_fields
            ? []
            : $data;
    }

    private function rowFailsValidation(string $field, array $data): bool
    {
        if ($field === 'vin') {
            return strlen((string) $data[$field]) < 17;
        }

        return false;
    }

    private function ensureFormattingForField(mixed $on_model, $value, $row, ?string $valueCastType = null): mixed
    {
        $country = Settings::get('app-details-country') ?? config('app.default_country');
        return match ($on_model) {
            'registration_number' => Country::normalizeNumberPlate($country, $value ?? '', false),
            'make_id' => $this->getMappedTaxonomy(TaxonomyMap::MAKE, $value, $row),
            'model_id' => $valueCastType === 'model_from_title'
                ? $this->castToModelFromTitle($value, $row)
                : $this->getMappedTaxonomy(TaxonomyMap::MODEL, $value, $row),
            'transmission_id' => $this->getMappedTaxonomy(TaxonomyMap::TRANSMISSION, $value, $row),
            'body_style_id' => $this->getMappedTaxonomy(TaxonomyMap::BODY_STYLE, $value, $row),
            'drivetrain_id' => $this->getMappedTaxonomy(TaxonomyMap::DRIVETRAIN, $value, $row),
            'fuel_type_id' => $this->getMappedTaxonomy(TaxonomyMap::FUEL_TYPE, $value, $row),
            'type' => $this->resolveVehicleType($value, $row),
            'stock_arrival_date' => $valueCastType ? $value : $this->castToDateTime($value),
            'first_registration_date' => $valueCastType === 'date_from_vrm'
                ? $this->castToDateFromVrm($row)
                : ($valueCastType ? $value : $this->castToDateTime($value)),
            'dealership_id' => $this->getDealershipId($value),
            default => is_string($value) ? trim($value) : $value,
        };
    }

    private function decodeDefaultValue($default, array $row)
    {
        // If not logic block, return literal value
        if (!(is_string($default) && Str::startsWith($default, '{{') && Str::endsWith($default, '}}'))) {
            return $default;
        }

        // Template mode: two or more {{column}} tokens are replaced with their row values and
        // concatenated with any literal text between them, e.g. "{{model_desc}} {{model_code}}"
        // => "SPRAYER OKTOPUS". A single {{...}} block keeps the logic/expression behaviour below.
        if (preg_match_all('/\{\{\s*([^{}]+?)\s*\}\}/', $default) > 1) {
            return trim(preg_replace_callback(
                '/\{\{\s*([^{}]+?)\s*\}\}/',
                fn ($matches) => trim((string) ($row[$matches[1]] ?? '')),
                $default
            ));
        }

        // Strip logic identifiers
        $lookup = str_replace(['{{', '}}'], '', $default);
        $matched_operator = collect([
            '>=',
            '<=',
            '!=',
            '>',
            '<',
            '=',
            '+',
            '-',
            '*',
            '/',
        ])
            ->filter(fn ($operator) => stripos($lookup, $operator) !== false)
            ->first();

        // not a supported operator, return column with specified name or null if no column exists
        if (!$matched_operator) {
            return $row[$lookup] ?? null;
        }

        $conditions = explode($matched_operator, $lookup);

        // If arithmetic operator matched but right side is not numeric, fall back to column reference
        // This protects column names like "auto-trader" from being treated as subtraction
        if (in_array($matched_operator, ['+', '-', '*', '/']) && !is_numeric(trim($conditions[1]))) {
            return $row[$lookup] ?? null;
        }

        // Perform a logic check based on left side being a column in file and right side being a value to check against
        return match ($matched_operator) {
            '>=' => ($row[$conditions[0]] ?? '') >= $conditions[1],
            '<=' => ($row[$conditions[0]] ?? '') <= $conditions[1],
            '!=' => ($row[$conditions[0]] ?? '') != $conditions[1],
            '>' => ($row[$conditions[0]] ?? '') > $conditions[1],
            '<' => ($row[$conditions[0]] ?? '') < $conditions[1],
            '=' => ($row[$conditions[0]] ?? '') == $conditions[1],
            '+' => $this->parseNumericValue($row[$conditions[0]] ?? 0) + (float) $conditions[1],
            '-' => $this->parseNumericValue($row[$conditions[0]] ?? 0) - (float) $conditions[1],
            '*' => $this->parseNumericValue($row[$conditions[0]] ?? 0) * (float) $conditions[1],
            '/' => (float) $conditions[1] != 0
                ? $this->parseNumericValue($row[$conditions[0]] ?? 0) / (float) $conditions[1]
                : $this->parseNumericValue($row[$conditions[0]] ?? 0),
            default => $conditions[0],
        };
    }

    /**
     * Feeds regularly format numbers for display ("£ 26,750", "26,500.50"), which a
     * plain float cast reads as 0 or truncates at the first non-numeric character.
     */
    private function parseNumericValue(mixed $value): float
    {
        if (is_int($value) || is_float($value)) {
            return (float) $value;
        }

        return (float) preg_replace('/[^0-9.\-]/', '', (string) $value);
    }

    private function processImageFiles(array $images): void
    {
        collect(File::files(Storage::disk('local')->path($this->getArchiveFolder())))
            ->filter(fn (SplFileInfo $file) => in_array($file->getFilename(), $images))
            ->tap(fn (Collection $chunk) => $this->importImageBatch($chunk));
    }

    private function importImageBatch(Collection $chunk): void
    {
        $file_names = $chunk->map(fn (SplFileInfo $file) => $file->getFilename());
        $existing = \Mtc\MercuryDataModels\Media::query()
            ->withCount('uses')
            ->where('media.image_provider', $this->getProviderName())
            ->whereIn('media.source_filename', $file_names)
            ->get();

        $disk = Storage::disk(Config::get('filesystems.default_media'));
        $existingFilenames = $existing
            ->reject(function ($media) use ($disk) {
                if ($media->uses_count > 0) {
                    return false;
                }
                // Orphaned record whose file no longer exists — drop it so the
                // import path below re-creates a fresh media record from the file.
                if (!$disk->exists($media->getOriginalFilePath())) {
                    $media->delete();
                    return true;
                }
                return false;
            })
            ->pluck('source_filename');

        $chunk->reject(fn (SplFileInfo $file) => $existingFilenames->search($file->getFilename()) !== false)
            ->each(fn (SplFileInfo $file) => MediaFacade::importImageFromFile($file, $this->getProviderName()));
    }

    protected function getArchiveFolder(): string
    {
        if (empty($this->archive_folder)) {
            $this->archive_folder = uniqid('zip_extract_', true);
        }

        return $this->archive_folder;
    }

    private function syncImagesFromFile(Vehicle $vehicle, array $images): void
    {
        $mediaMap = \Mtc\MercuryDataModels\Media::query()
            ->whereIn('source_filename', $images)
            ->where('image_provider', $this->getProviderName())
            ->pluck('id', 'source_filename');

        $media = collect($images)
            ->map(fn (string $image) => $mediaMap->get($image))
            ->filter()
            ->values()
            ->toArray();

        MediaFacade::setUsesForModel($media, $vehicle, ['primary' => true]);
    }
}
