<?php

namespace App\Modules\Stock;

use App\Contracts\StockProvider;
use App\Events\NewVehicleImported;
use App\Events\StockSyncFinished;
use App\Events\VehicleUpdatedFromImport;
use App\Facades\Settings;
use App\TaxonomyMap;
use App\Traits\ImportChecksConditions;
use App\Traits\MapsTaxonomies;
use App\Traits\StockSyncTraits;
use App\VehicleType;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Mtc\ContentManager\Facades\Media as MediaFacade;
use Mtc\MercuryDataModels\Media;
use Mtc\MercuryDataModels\Vehicle;
use SimpleXMLElement;
use Symfony\Component\Finder\SplFileInfo;
use ZipArchive;

class Modix implements StockProvider
{
    use StockSyncTraits;
    use MapsTaxonomies;
    use ImportChecksConditions;

    /*
     * Date IDs from Modix documentation
     */
    private const DATE_ID_MANUFACTURE_DATE = 1;
    private const DATE_ID_FIRST_REGISTRATION = 2;
    private const DATE_ID_GENERAL_INSPECTION = 3;
    private const DATE_ID_EXHAUST_INSPECTION = 4;

    private Collection $dealerships;

    public function enabled(): bool
    {
        return Settings::get('stock-modix-enabled') ?? false;
    }

    protected function getProviderName(): string
    {
        return 'modix';
    }

    public function name(): string
    {
        return __('labels.stock_providers.modix');
    }

    /**
     * Fields to add to dealership management
     *
     * @return array[]
     */
    public function dealershipAdditionalDataFields()
    {
        return [
            'modix-location-id' => [
                'type' => 'text',
                'label' => __('labels.stock_providers.modix_dealer_field'),
            ],
        ];
    }

    public function runScheduledImport(bool $fullSync = true): void
    {
        $this->markVehiclesToBeSynced();
        $this->getAllStockLocations('modix-location-id');
        $this->retrieveAndExtractStockData();
        $this->removeNotSynced();

        Event::dispatch(new StockSyncFinished($this->getProviderName()));
    }

    private function processStockFile(string $stock_path): void
    {
        /** @var SplFileInfo $stock_file */
        $stock_file = collect(File::files($stock_path))
            ->filter(fn($filename) => pathinfo($filename, PATHINFO_EXTENSION) === 'xml')
            ->first();

        $content = File::get($stock_file->getPathname());
        $xml_element = new SimpleXMLElement($content);

        collect($xml_element->xpath('//vehicles/vehicle'))
            ->map(fn($vehicle) => $this->mapVehicleData($vehicle))
            ->each(fn(array $vehicle_data) => $this->syncVehicle($vehicle_data, $stock_path));
    }

    private function processImageFiles(string $stock_path, array $images): void
    {
        collect(File::files($stock_path))
            ->filter(fn($filename) => in_array($filename->getFilename(), $images))
            ->tap(fn(Collection $chunk) => $this->importImageBatch($chunk));
    }

    private function mapVehicleData(SimpleXMLElement $vehicle): array
    {
        $make_id = $this->getMappedTaxonomy(
            TaxonomyMap::MAKE,
            (string)$vehicle->mainData->manufacturer,
            (array)$vehicle
        );

        $tech_data = [];
        foreach ($vehicle->options->option as $option) {
            $tech_data[(string)$option->description] = (string)$option->name ?: (string)$option->value;
        }

        $fuel_type = (string)$vehicle->mainData->fuel;
        if (!empty($tech_data['Plugin hybrid']) && $tech_data['Plugin hybrid'] == 'yes') {
            $fuel_type .= ' Plug-in Hybrid';
        }

        $vehicle_data = array_filter([
            'uuid' => (string)$vehicle['cipher'],
            'registration_number' => strtoupper((string)$vehicle['internalDescription']),
            'derivative' => (string)$vehicle->mainData->submodel,
            'type' => match ((string)$vehicle->mainData->category) {
                'Passenger car' => VehicleType::CAR->value,
                default => VehicleType::CAR->value,
            },
            'is_new' => match ((string)$vehicle->mainData->usageCategory) {
                'Used vehicle' => 0,
                default => 1,
            },
            'dealership_id' => $this->dealershipId((string)$vehicle['modkey']),
            'make_id' => $make_id,
            'model_id' => $this->getMappedTaxonomy(
                TaxonomyMap::MODEL,
                (string)$vehicle->mainData->modelText,
                (array)$vehicle
            ),
            'fuel_type_id' => $this->getMappedTaxonomy(
                TaxonomyMap::FUEL_TYPE,
                $fuel_type,
                (array)$vehicle
            ),
            'transmission_id' => $this->getMappedTaxonomy(
                TaxonomyMap::TRANSMISSION,
                (string)$vehicle->mainData->transmission,
                (array)$vehicle
            ),
            'body_style_id' => $this->getMappedTaxonomy(
                TaxonomyMap::BODY_STYLE,
                (string)$vehicle->mainData->bodyStyle,
                (array)$vehicle
            ),
            'bhp' => (int)$vehicle->mainData->enginePower,
            'odometer_mi' => (int)$vehicle->mainData->mileage,
            'engine_cc' => (int)$vehicle->mainData->cylinderCapacity,
            'is_published' => true,
            'vin' => (string)$vehicle->identification->vin,
            'colour' => (string)$vehicle->colors->bodyColors->bodyColor->description
                ?: (string)$vehicle->colors->bodyColors->bodyColor->name,
            'description' => (string)$vehicle->description,
            'price' => (float)$vehicle->prices->price->grossPrice,
            'was_recently_synced' => true,
            'stock_provider' => $this->getProviderName(),
        ], fn($value) => !is_null($value));

        $first_registration_date = $this->getFirstRegistrationDate($vehicle);
        if (!empty($first_registration_date)) {
            $vehicle_data['first_registration_date'] = $first_registration_date;
        }

        if (empty($vehicle_data['registration_number']) && !empty($tech_data['Registration plate number'])) {
            $vehicle_data['registration_number'] = $tech_data['Registration plate number'];
        }

        if (!empty($tech_data['Description text (formatted)'])) {
            $vehicle_data['description'] = $tech_data['Description text (formatted)'];
        }

        $manufacture_date = $this->getManufactureDate($vehicle);
        if (!empty($manufacture_date)) {
            try {
                $date = Carbon::parse($manufacture_date);
                $vehicle_data['manufacture_year'] = $date->format('Y');
            } catch (\Exception $exception) {
                // Do nothing
            }
        }

        $status = (int)$vehicle->mainData->vehicleStatus;
        if ($status === 120) {
            $vehicle_data['is_reserved'] = true;
        }
        if ($status === 130) {
            $vehicle_data['is_sold'] = true;
        }
        if ($status === 100) {
            $vehicle_data['is_reserved'] = false;
            $vehicle_data['is_sold'] = false;
        }

        if (!empty($tech_data['Doors'])) {
            $vehicle_data['door_count'] = (int)$tech_data['Doors'];
        }
        if (!empty($tech_data['Seats'])) {
            $vehicle_data['seats'] = (int)$tech_data['Seats'];
        }
        if (!empty($tech_data['WLTP consumption combined'])) {
            $vehicle_data['mpg'] = (int)$tech_data['WLTP consumption combined'];
        }
        if (!empty($tech_data['Special equipment'])) {
            $vehicle_data['features'] = array_unique(explode(', ', $tech_data['Special equipment']));
        }
        if (!empty($tech_data['Standard equipment'])) {
            $vehicle_data['equipment'] = array_unique(explode(', ', $tech_data['Standard equipment']));
        }
        if (!empty($tech_data['Model year'])) {
            $vehicle_data['model_year'] = (int)$tech_data['Model year'];
        }
        if (!empty($tech_data['Guarantee program'])) {
            $vehicle_data['attention_grabber'] = (string)$tech_data['Guarantee program'];
        }
        if (!empty($tech_data['Cap ID'])) {
            $vehicle_data['cap_id'] = (string)$tech_data['Cap ID'];
        }
        if (!empty($tech_data['Battery capacity'])) {
            $vehicle_data['battery_capacity_kwh'] = (int)$tech_data['Battery capacity'] ?: null;
            $vehicle_data['battery_usable_capacity_kwh'] = (int)$tech_data['Battery capacity'] ?: null;
        }
        if (!empty($tech_data['NEDC electric range'])) {
            $vehicle_data['battery_range'] = (int)$tech_data['NEDC electric range'] ?: null;
        }

        $media = [];
        foreach ($vehicle->media->images->image as $image) {
            $media[] = (string)$image['name'];
        }
        $vehicle_data['images'] = $media;

        return $vehicle_data;
    }

    private function syncVehicle(array $vehicle_data, string $stock_path): void
    {
        $vehicle = Vehicle::query()->firstOrNew([
            'uuid' => $vehicle_data['uuid'],
            'stock_provider' => $this->getProviderName(),
        ]);

        $vehicle->is_published = $this->shouldBePublished(
            fn() => $vehicle->is_published ?? true,
            'modix',
            $vehicle,
        ) ?? false;

        $vehicle_previous_price = $vehicle->price;
        $vehicle->fill($vehicle_data)->save();

        // Only trigger price change events if the prices are numerically different.
        // It is possible that wasChanged() could return true for certain type casts.
        if ($vehicle->wasChanged('price')) {
            $this->priceChangeEvents($vehicle, $this->getProviderName(), $vehicle_previous_price);
        }

        if (!empty($vehicle_data['equipment']) && $vehicle->equipment()->count() != count($vehicle_data['equipment'])) {
            $this->syncEquipment($vehicle, $vehicle_data['equipment']);
        }
        $this->syncFeatures($vehicle, $vehicle_data['features'] ?? []);

        if (!empty($vehicle_data['images']) && $vehicle->mediaUses()->count() == 0) {
            $this->processImageFiles($stock_path, $vehicle_data['images']);
            $this->syncImages($vehicle, $vehicle_data['images']);
        }

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

    private function syncEquipment(Vehicle $vehicle, array $equipment): void
    {
        collect($equipment)
            ->filter()
            ->each(fn($entry) => $vehicle->equipment()->updateOrCreate([
                'description' => $entry
            ]));
    }

    private function syncFeatures(Vehicle $vehicle, array $features): void
    {
        $features_to_keep = collect($features)
            ->filter()
            ->each(fn($entry) => $vehicle->features()->updateOrCreate([
                'name' => $entry
            ]));

        $vehicle->features()->whereNotIn('name', $features_to_keep)->delete();
    }

    private function syncImages(Vehicle $vehicle, array $images): void
    {
        $media = Media::query()
            ->whereIn('source_filename', $images)
            ->where('image_provider', $this->getProviderName())
            ->pluck('id')
            ->toArray();
        MediaFacade::setUsesForModel($media, $vehicle, ['primary' => true]);
    }

    private function retrieveAndExtractStockData(): void
    {
        $disk = Storage::build([
            'driver' => 'ftp',
            'host' => Settings::get('stock-modix-ftp-host'),
            'username' => Settings::get('stock-modix-ftp-username'),
            'password' => Settings::get('stock-modix-ftp-password'),
            'ssl' => null,
        ]);

        $archiveFolder = storage_path(uniqid('zip_extract_', true));

        if (!empty(Settings::get('stock-modix-archive-path'))) {
            if ($disk->exists(Settings::get('stock-modix-archive-path'))) {
                Storage::disk('local')->put(
                    Settings::get('stock-modix-archive-path'),
                    $disk->get(Settings::get('stock-modix-archive-path'))
                );

                $this->extractZipArchive(
                    Storage::disk('local')->path(Settings::get('stock-modix-archive-path')),
                    $archiveFolder
                );

                $this->processStockFile($archiveFolder);
                $this->removeLocalFiles($archiveFolder);
            }
        } else {
            collect($disk->files())->filter(fn($file) => Str::endsWith($file, '.zip'))
                ->each(function ($archive) use ($disk, $archiveFolder) {
                    Storage::disk('local')->put(
                        $archive,
                        $disk->get($archive)
                    );

                    $this->extractZipArchive(
                        Storage::disk('local')->path($archive),
                        $archiveFolder
                    );

                    $this->processStockFile($archiveFolder);
                    $this->removeLocalFiles($archiveFolder);
                });
        }
    }

    private function extractZipArchive(string $archivePath, string $extractPath): void
    {
        $zip = new ZipArchive();

        if ($zip->open($archivePath) === true) {
            $zip->extractTo($extractPath);
            $zip->close();
        } else {
            throw new \Exception("Unable to extract the ZIP file");
        }
    }

    private function importImageBatch(Collection $chunk): void
    {
        $file_names = $chunk->map(fn(SplFileInfo $file) => $file->getFilename());
        $existing = 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()));
    }

    private function removeLocalFiles(string $stock_path): void
    {
        File::deleteDirectory($stock_path);
    }

    /**
     * Get a date by id.
     * We expect the dates data to look like:
     *      <dates>
     *          <date id="2">2022-06-24</date>
     *          <date id="6">2020-02-06</date>
     *          <date id="7">2024-03-07</date>
     *      </dates>
     *
     * @param SimpleXMLElement $vehicle
     * @param $date_id
     * @return string|null
     */
    private function getDateById(SimpleXMLElement $vehicle, $date_id): ?string
    {
        $date = $vehicle->xpath('./dates/date[@id="' . $date_id . '"]');
        return (string)($date[0] ?? null);
    }

    private function getManufactureDate(SimpleXMLElement $vehicle): ?string
    {
        return $this->getDateById($vehicle, self::DATE_ID_MANUFACTURE_DATE);
    }

    private function getFirstRegistrationDate(SimpleXMLElement $vehicle): ?string
    {
        return $this->getDateById($vehicle, self::DATE_ID_FIRST_REGISTRATION);
    }
}
