<?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\Jobs\ImportImagesFromUrlList;
use App\Services\CurrencyService;
use App\TaxonomyMap;
use App\Traits\EnsuresVehicleAttribute;
use App\Traits\ImportChecksConditions;
use App\Traits\MapsTaxonomies;
use App\Traits\StockSyncTraits;
use App\Traits\TracksImportStats;
use App\Traits\VehicleRegistrationDateFinderTrait;
use Mtc\MercuryDataModels\VehicleType;
use Carbon\Carbon;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Mtc\ContentManager\Models\MediaUse;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Vehicle;

class Skupenet implements StockProvider
{
    use DispatchesJobs;
    use EnsuresVehicleAttribute;
    use ImportChecksConditions;
    use MapsTaxonomies;
    use StockSyncTraits;
    use TracksImportStats;
    use VehicleRegistrationDateFinderTrait;

    private Collection $dealerships;
    private array $make = [];
    private array $model = [];
    private array $transmission = [];
    private array $body_style = [];
    private array $fuel_type = [];
    private array $vehicleAttributes;

    private const string ENDPOINT = 'https://feeds.dealerhub.ie/api/v1/stock?';

    // Bullet lines only count as features within a run of at least this many lines,
    // so a lone "- trade-ins welcome" style prose line is never extracted
    private const int MIN_BULLET_RUN = 3;

    private const string FEATURE_HEADING_PATTERN =
        '/^\s*(?:key\s+)?(?:features?|specs?|specifications?|equipment|highlights)\s*:?\s*$/i';
    /**
     * Check if enabled
     *
     * @return bool
     */
    public function enabled(): bool
    {
        return Settings::get('stock-skupenet-enabled', false) ?? false;
    }

    /**
     * Name of the integration
     *
     * @return string
     */
    public function name(): string
    {
        return 'Skupenet';
    }

    /**
     * Perform a scheduled import task
     *
     * @return void
     */
    public function runScheduledImport(bool $fullSync = true): void
    {
        $this->resetStats();
        $this->markVehiclesToBeSynced();
        $this->retrieveRequiredVehicleAttributes();
        $this->import($this->fetchVehicles());
    }

    /**
     * Fields to add to dealership management
     *
     * @return array[]
     */
    public function dealershipAdditionalDataFields()
    {
        return [
            'skupenet-location-id' => [
                'type' => 'text',
                'label' => 'Seller ID within Skupenet DMS',
            ],
        ];
    }

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

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

    /**
     * Fetch vehicles from API
     *
     * @return Collection
     */
    private function fetchVehicles(): Collection
    {
        return $this->getStockForLocations($this->getAllStockLocations('skupenet-location-id'))
            ->unique('id');
    }

    private function getStockForLocations(Collection $locations): Collection
    {
        if ($locations->isEmpty()) {
            throw new \Exception('No locations provided for Skupenet stock retrieval ' . tenant('id'));
        }

        $params = [
            'dealerIds' => $locations->implode(','),
            'pageSize' => 50,
            'page' => 1
        ];
        $response = Http::withHeaders(['X-API-KEY' => config('services.skupenet.api_key')])
            ->get(self::ENDPOINT . http_build_query($params));

        if (!$response->successful()) {
            throw new \Exception('Failed to retrieve vehicle stock from Skupenet ' . tenant('id'));
        }
        $vehicles = collect($response->json('stock'));
        $numPages = $response->json('paging.totalPages') ?? 1;

        for ($page = 2; $page <= $numPages; $page++) {
            $params['page'] = $page;
            $response = Http::withHeaders(['X-API-KEY' => config('services.skupenet.api_key')])
                ->get(self::ENDPOINT . http_build_query($params));

            $vehicles = $vehicles->merge($response->json('stock'));
        }

        return $vehicles;
    }

    /**
     * Import vehicles
     *
     * @param Collection $stock
     * @return void
     */
    private function import(Collection $stock)
    {
        if ($stock->count() === 0) {
            // No vehicles to sync, assume service failure and stop sync to avoid removing vehicles
            return;
        }
        $stock = $stock->filter(fn ($vehicle) => $this->filterByPrice($vehicle));

        $stock->each(fn (array $vehicle) => $this->syncVehicle($vehicle));

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

    /**
     * Filter out vehicles depending on price settings
     *
     * @param array $vehicle
     * @return bool
     */
    private function filterByPrice(array $vehicle): bool
    {
        if (Settings::get('stock-skupenet-sync-with-price') !== true) {
            return true;
        }

        if (empty($vehicle['price']) || !is_numeric($vehicle['price'])) {
            return false;
        }

        return $vehicle['price'] > 0;
    }

    /**
     * Sync vehicle record
     *
     * @param array $data
     * @return void
     */
    private function syncVehicle(array $data): void
    {
        $make_id = $this->getMappedTaxonomy(TaxonomyMap::MAKE, $data['attributes']['make'] ?? '', $data);

        /** @var Vehicle $vehicle */
        /** @var Vehicle $vehicle */
        $vehicle = Vehicle::query()
            ->where('uuid', $data['id'])
            ->firstOrNew([
                'stock_provider' => $this->getProviderName(),
                'uuid' => $data['id'],
            ]);
        $mileageType = strtolower($data['attributes']['mileageUnit'] ?? 'kms') === 'km' ? 'km' : 'mi';

        if ($this->shouldSyncVideo($vehicle, $data)) {
            $vehicle['main_video_url'] = $this->getVideoUrl($data);
        }

        if (!empty($data['attributes']['make']) && !empty($data['attributes']['model'])) {
            $title = $data['attributes']['make'] . ' ' . $data['attributes']['model'];
        } else {
            $title = $data['title'];
        }

        [$description, $features] = $this->extractDescriptionAndFeatures($data['description'] ?? '');
        $vehicle->fill([
            'title' => $title,
            'registration_number' => $data['attributes']['registration'] ?? null,
            'first_registration_date' => $this->determineRegistrationDate($data),
            'stock_arrival_date' => $this->determineStockArrivalDate($vehicle, $data),
            'derivative' => $data['attributes']['trim'] ?? $data['attributes']['trimVersion'] ?? '',
            'make_id' => $make_id,
            'model_id' => $this->getMappedTaxonomy(
                TaxonomyMap::MODEL,
                $data['attributes']['model'] ?? '',
                $data,
                $make_id
            ),
            'colour' => $data['attributes']['colour'] ?? null,
            'fuel_type_id' => $this->getMappedTaxonomy(
                TaxonomyMap::FUEL_TYPE,
                $data['attributes']['fuelType'] ?? null,
                $data
            ),
            'body_style_id' => $this->getMappedTaxonomy(
                TaxonomyMap::BODY_STYLE,
                $data['attributes']['bodyType'] ?? null,
                $data
            ),
            'type' => $this->mapVehicleType($data['stockType'], $data['attributes']['bodyType'] ?? null),
            'bhp' => $data['attributes']['enginePowerBhp'] ?? null,
            'engine_size_cc' => $data['attributes']['engineSizeCC'] ?? null,
            'seats' => $data['attributes']['numSeats'] ?? null,
            'previous_owner_count' => $data['attributes']['numOwners'] ?? null,
            'door_count' => $data['attributes']['numDoors'] ?? null,
            'transmission_id' => $this->getMappedTaxonomy(
                TaxonomyMap::TRANSMISSION,
                $data['attributes']['transmission'] ?? null,
                $data
            ),
            'manufacture_year' => $data['attributes']['year'] ?? null,
            'was_recently_synced' => true,
            'is_new' => ($data['attributes']['mileage'] ?? 1)
                <= Settings::get('stock-skupenet-new-vehicle-mileage-threshold'),
            'dealership_id' => $this->dealershipId($data['dhfDealerId']),
            'odometer_mi' => $mileageType === 'km'
                ? $vehicle->kmToMiles($data['attributes']['mileage'] ?? 0)
                : $data['attributes']['mileage'] ?? 0,
            'odometer_km' => $mileageType === 'km'
                ? $data['attributes']['mileage'] ?? 0
                : $vehicle->milesToKm($data['attributes']['mileage'] ?? 0),
            'price' => $this->getPrice($data),
            'description' => $description,
            'battery_capacity_kwh' => $data['attributes']['batteryCapacityKwh'] ?? null,
            'battery_range' => $data['attributes']['batteryRangeKm'] ?? null,
        ]);
        $vehicle->is_published = $this->shouldBePublished(fn () => ($data['price'] ?? 0) > 1, 'skupenet', $vehicle);
        $vehicle->save();

        $this->setVehicleAttributes(
            [
                'nct_due_date' => $data['attributes']['nctDueDate'] ?? null,
                'road_tax' => $data['attributes']['roadTax'] ?? null,
                'warranty' => $data['attributes']['warrantyDuration'] ?? null,
                'warranty_type' => $data['attributes']['warrantyType'] ?? null,
            ],
            $vehicle
        );

        if ($this->shouldSyncImages($vehicle, $data)) {
            $this->syncImages($vehicle, $data['images'] ?? []);
        }

        $this->syncFeatures($vehicle, $features);

        $this->storeUnmappedTaxonomy($vehicle);

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

    private function mapVehicleType(string $stockType, ?string $bodyType = null): string
    {
        $type = match (true) {
            stripos($stockType, 'lcv') !== false => VehicleType::LCV->value,
            stripos($stockType, 'bike') !== false,
            stripos($stockType, 'rad') !== false => VehicleType::MOTORCYCLE->value,
            $stockType === 'COMMERCIALS' => VehicleType::LCV->value,
            $stockType === 'CARS' => VehicleType::CAR->value,
            default => VehicleType::CAR->value,
        };

        if ($bodyType === 'Commercial/Van') {
            $type = VehicleType::LCV->value;
        }

        return $type;
    }

    /**
     * Check if images should be synced for vehicles
     *
     * @param Vehicle $vehicle
     * @param array $data
     * @return bool
     */
    private function shouldSyncImages(Vehicle $vehicle, array $data): bool
    {
        $images = collect($data['images'] ?? [])->map(fn ($image) => rtrim((string) $image['url'], '?'))->toArray();
        $images_from_other_provider = $vehicle->mediaUses()
            ->whereHas('media', fn ($query) => $query->where('image_provider', '!=', 'skupenet'))
            ->count();
        return Settings::get('stock-skupenet-sync-images')
            && $images_from_other_provider == 0
            && $this->imageChecksumMismatch($images, $vehicle);
    }

    private function shouldSyncVideo(Vehicle $vehicle, array $data): bool
    {
        return Settings::get('stock-skupenet-sync-video', false);
    }

    /**
     * Sync images for vehicle
     *
     * @param Vehicle $vehicle
     * @param array $images
     * @return void
     */
    private function syncImages(Vehicle $vehicle, array $images)
    {
        $imageList = collect($images)
         ->sortBy('sortOrder')
        ->map(fn ($image) => rtrim((string) $image['url'], '?'))
         ->values();

        $to_remove = $this->getImagesToRemove($vehicle, $imageList->toArray());
        MediaUse::query()->whereIn('id', $to_remove)->delete();
        $this->dispatch(new ImportImagesFromUrlList($imageList, $vehicle, false, 'skupenet'));
    }

    /**
     * handle engine sizes cc which may contain text e.g. 'litre'
     * @param bool $value
     */
    private function getEngineSize($value = false)
    {
        $value = strtoupper($value);
        $value = str_replace('LITRES', '', $value);
        $value = str_replace('LITRE', '', $value);
        $value = str_replace(' ', '', $value);

        if (is_numeric($value)) {
            return $value * 1000;
        }

        return false;
    }

    /**
     * Get currency type from value
     *
     * @param string $value
     * @return string
     */
    private function getCurrencyType(string $value = ''): string
    {
        $type = 'EUR';

        if (stripos($value, 'gbp') !== false || stripos($value, 'pound') !== false) {
            $type = 'GBP';
        }

        return $type;
    }

    /**
     * Get price in default currency
     *
     * @param array $data
     * @return float|int|string
     */
    private function getPrice(array $data = []): float|int|string
    {
        $price = isset($data['price']) && is_numeric($data['price']) ? $data['price'] : 0;
        $priceCurrency = $this->getCurrencyType($data['currency']);

        return App::make(CurrencyService::class)->convertToCurrency($price, $priceCurrency);
    }

    /**
     * Split the raw feed description into the stored description text and the feature
     * lines, using the strategy selected by the stock-skupenet-feature-source setting.
     *
     * @return array{0: string, 1: array}
     */
    private function extractDescriptionAndFeatures(string $rawDescription): array
    {
        return match (Settings::get('stock-skupenet-feature-source', 'description-tail')) {
            'description-bullets' => $this->extractBulletFeatures($rawDescription),
            'none' => [trim(strip_tags($rawDescription)), []],
            default => $this->extractTailBlockFeatures($rawDescription),
        };
    }

    /**
     * Default behaviour: treat the last double-newline block of the description as the
     * feature list when it contains more than 5 lines.
     */
    private function extractTailBlockFeatures(string $rawDescription): array
    {
        $features = [];
        $description_data = explode("\n\n", $rawDescription);
        if (count($description_data) > 1) {
            // We have no clear way of knowing if this is description or feature list since clients can
            // use double newline as well in description, but we assume that over 5 newlines will be features
            if (count(explode("\n", end($description_data))) > 5) {
                $features = explode("\n", array_pop($description_data));
            }
            $description_data[0] = implode("\n", $description_data);
        }

        return [trim(strip_tags($description_data[0])), $features];
    }

    /**
     * Extract bullet lines anywhere in the description as features. A line qualifies when
     * it starts with one of the configured markers followed by whitespace, within a run of
     * at least MIN_BULLET_RUN such lines. Extracted lines are removed from the description,
     * along with a recognised feature heading (e.g. "KEY FEATURES") directly above the run.
     */
    private function extractBulletFeatures(string $rawDescription): array
    {
        $markerPattern = $this->bulletMarkerPattern();
        if ($markerPattern === null) {
            return [trim(strip_tags($rawDescription)), []];
        }

        $lines = explode("\n", $rawDescription);
        $features = [];
        $removed = [];
        $run = [];

        $flushRun = function () use (&$run, &$features, &$removed, $lines) {
            if (count($run) >= self::MIN_BULLET_RUN) {
                foreach ($run as [$index, $name]) {
                    $features[] = $name;
                    $removed[$index] = true;
                }

                // Drop a recognised heading directly above the run (blank lines in between allowed)
                $headingIndex = $run[0][0] - 1;
                while ($headingIndex >= 0 && trim($lines[$headingIndex]) === '') {
                    $headingIndex--;
                }
                if ($headingIndex >= 0 && preg_match(self::FEATURE_HEADING_PATTERN, $lines[$headingIndex])) {
                    $removed[$headingIndex] = true;
                }
            }
            $run = [];
        };

        foreach ($lines as $index => $line) {
            if (preg_match($markerPattern, $line, $matches)) {
                $run[] = [$index, trim($matches[1])];
            } elseif (trim($line) !== '') {
                // Prose breaks a run; blank lines neither extend nor break it
                $flushRun();
            }
        }
        $flushRun();

        $description = collect($lines)
            ->reject(fn ($line, $index) => isset($removed[$index]))
            ->implode("\n");
        $description = trim(strip_tags(preg_replace("/\n\s*\n(?:\s*\n)+/", "\n\n", $description)));

        return [$description, $features];
    }

    /**
     * Regex matching a feature bullet line, built from the per-tenant marker list.
     */
    private function bulletMarkerPattern(): ?string
    {
        $markers = collect(explode(',', (string) Settings::get('stock-skupenet-feature-markers', '•,-,*')))
            ->map(fn ($marker) => trim($marker))
            ->filter()
            ->map(fn ($marker) => preg_quote($marker, '/'));

        if ($markers->isEmpty()) {
            return null;
        }

        return '/^\s*(?:' . $markers->implode('|') . ')\s+(\S.*)$/u';
    }

    private function syncFeatures(Vehicle $vehicle, array $features): void
    {
        if (Settings::get('stock-skupenet-feature-source') === 'description-bullets') {
            $this->replaceFeatures($vehicle, $features);
            return;
        }

        if (!empty($features)) {
            $this->syncOptions($vehicle, $features);
        }
    }

    /**
     * Authoritative sync for the bullet feature source: the feed is the source of truth,
     * so features removed at Skupenet's end are removed here as well.
     */
    private function replaceFeatures(Vehicle $vehicle, array $features): void
    {
        $feature_names = collect($features)
            ->map(fn ($name) => trim(strip_tags((string) $name)))
            ->reject(fn ($name) => $name === '' || strlen($name) > 180)
            ->unique()
            ->values();

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

        $feature_names->each(fn ($name) => $vehicle->features()->updateOrCreate([
            'vehicle_type' => $vehicle->getMorphClass(),
            'name' => $name,
        ]));
    }

    private function syncOptions(Vehicle $vehicle, array $options)
    {
        collect($options)
            ->reject(fn ($optionData) => strlen((string) $optionData) > 180
                || empty(trim(str_replace("•", "", $optionData))))
            ->each(fn ($optionData) => $vehicle->features()->updateOrCreate([
                'vehicle_type' => $vehicle->getMorphClass(),
                'name' => trim(str_replace("•", "", $optionData)),
            ]));
    }

    private function getVideoUrl(array $data): ?string
    {
        if (empty($data['videos'][0]['url'])) {
            return null;
        }

        try {
            return $data['videos'][0]['url'];
        } catch (\Exception) {
            return null;
        }
    }

    private function getImagesToRemove(?Vehicle $vehicle, array $images)
    {
        $media_uses = $vehicle->mediaUses()->with('media')
            ->whereHas('media', fn ($query) => $query->where('image_provider', 'skupenet'))
            ->get();

        $not_in_list = $media_uses
            ->filter(fn ($mediaUse) => !in_array($mediaUse->media->source_filename, $images))
            ->pluck('id');

        $duplicates = $media_uses->groupBy(fn ($mediaUse) => $mediaUse->media->source_filename)
            ->filter(fn (Collection $group) => $group->count() > 1)
            ->map(fn (Collection $group) => $group->first())
            ->pluck('id');

        return $not_in_list->merge($duplicates);
    }

    private function determineRegistrationDate(array $data): ?string
    {
        if (empty($data['attributes']['registration'])) {
            return null;
        }

        $registration_date = $this->getRegistrationDateFromRegistrationNumber($data['attributes']['registration']);

        if (empty($registration_date) && !empty($data['attributes']['year'])) {
            $registration_date = $data['attributes']['year'] . '-01-01';
        }

        if (
            Settings::get('stock-override-reg-year-with-model-year')
            && !empty($registration_date)
            && !empty($data['attributes']['year'])
        ) {
            $reg_date_obj = Carbon::parse($registration_date);
            $modelYear = (int) $data['attributes']['year'];

            if ($modelYear > $reg_date_obj->year) {
                // take model year as reg year
                $reg_date_obj->year($modelYear);
                // adjust reg month to January, to fall within UK registration date ranges (Mar-Sep, Sep-Mar)
                $reg_date_obj->month(1);
                return $reg_date_obj->format('Y-m-d');
            }
        }

        return !empty($registration_date)
            ? Carbon::parse($registration_date)
            : null;
    }

    /**
     * Skupenet reports when the listing was created in DealerHub; use that as the
     * stock arrival date, but never overwrite a value that is already set.
     */
    private function determineStockArrivalDate(Vehicle $vehicle, array $data): ?Carbon
    {
        if (!empty($vehicle->stock_arrival_date)) {
            return $vehicle->stock_arrival_date;
        }

        return !empty($data['createdAt'])
            ? Carbon::parse($data['createdAt'])->startOfDay()
            : null;
    }

    private function retrieveRequiredVehicleAttributes(): void
    {
        $this->vehicleAttributes = [
            'nct_due_date' => $this->getVehicleAttribute('NCT Due Date', 'datetime')->toArray(),
            'road_tax' => $this->getVehicleAttribute('Road Tax', 'number')->toArray(),
            'warranty_type' => $this->getVehicleAttribute('Warranty Type', 'text')->toArray(),
            'warranty' => $this->getVehicleAttribute('Warranty Duration', 'text')->toArray(),
        ];
    }
}
