<?php

namespace App\Imports\Traits;

use App\Events\NewVehicleImported;
use App\Events\VehicleUpdatedFromImport;
use App\Facades\Settings;
use App\Jobs\ImportImagesFromUrlList;
use App\Master\Models\VehicleMake;
use App\Master\Models\VehicleModel;
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\VehicleType;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
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 Symfony\Component\Finder\SplFileInfo;

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

    protected $subFieldDelimiter = '|';
    protected static array $allowedFieldsToUpdate = [];
    protected array $vehicleAttributes;
    protected bool $stock_sync = false;
    protected ?string $archive_folder = null;

    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'];
        }
        return $this;
    }

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

    protected function retrieveRequiredVehicleAttributes(): void
    {
        $this->vehicleAttributes = VehicleAttribute::all()->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 $model */
        $model = Vehicle::query()->firstOrNew($search_params);

        $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);
        }

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

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

        return $model;
    }

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

        $vehicle->autoTraderData()->updateOrCreate([
            'vehicle_id' => $vehicle->id
        ], [
            'publish_advert' => $this->castToBoolean($autotrader_enabled),
            'publish_profile' => $this->castToBoolean($autotrader_enabled),
        ]);
    }

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

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

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

    private function importRowSearchParams($row): ?array
    {
        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;
    }

    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))
            ->filter()
            ->each(fn($feature) => $vehicle->features()->updateOrCreate(['name' => $feature]));
    }

    private function imageAlreadyHostedOnServer($image): bool
    {
        try {
            return Media::assetExists($image);
        } catch (Exception $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 (empty($vehicle['stock_provider'])) {
            $vehicle['stock_provider'] = $this->getProviderName();
        }

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

    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' => (float)$value,
            'datetime' => $this->castToDateTime($value),
            'year_from_date' => $this->castToYearFromDate($value),
            'vehicle_type' => $this->castToVehicleType($value, $fieldConfig?->data['vehicle_type_mappings'] ?? []),
            default => $value,
        };
    }

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

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

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

    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 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 castToVehicleType($value, array $mappings): string
    {
        $normalizedValue = strtolower(trim($value ?? ''));

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

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

    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();
    }

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

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

    /**
     * Get the dealership id from stock location
     *
     * @param $locationId
     * @return string|null
     */
    private function getDealershipId($locationId): ?string
    {
        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($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' => $this->getMappedTaxonomy(TaxonomyMap::MODEL, $value, $row), // TODO: Needs make value for map
            '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' => match (strtolower($value ?? '')) {
                'lcv' => VehicleType::LCV->value,
                'motorcycle' => VehicleType::MOTORCYCLE->value,
                default => VehicleType::CAR->value,
            },
            // Try casting to datetime unless field is marked with cast type (which will ensure casting later)
            'stock_arrival_date',
            'first_registration_date' => $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;
        }

        // 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);
        // 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],
            default => $conditions[0],
        };
    }

    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()
            ->where('media.image_provider', $this->getProviderName())
            ->whereIn('media.source_filename', $file_names)
            ->pluck('media.source_filename');

        $chunk->reject(fn(SplFileInfo $file) => $existing->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
    {
        $media = \Mtc\MercuryDataModels\Media::query()
            ->whereIn('source_filename', $images)
            ->where('image_provider', $this->getProviderName())
            ->pluck('id')
            ->toArray();
        MediaFacade::setUsesForModel($media, $vehicle, ['primary' => true]);
    }
}
