<?php

namespace App\Modules\Stock;

use App\Contracts\StockProvider;
use App\Events\StockSyncFinished;
use App\Jobs\ImportDealerKitVehicleBatch;
use App\Modules\Stock\Config\DealerKitConfig;
use App\Services\DealerKitApi;
use App\Traits\TracksImportStats;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Media;
use Mtc\MercuryDataModels\Vehicle;

class DealerKit implements StockProvider
{
    use DispatchesJobs;
    use TracksImportStats;

    public const NAME = 'dealerkit';

    private const PER_PAGE = 50;

    /**
     * UUIDs of every vehicle seen in the feed this run, accumulated across the
     * paginated dispatchBatch() calls. Removal is driven off this in-memory set
     * so it stays correct regardless of when the queued batch jobs run.
     */
    private Collection $feedVehicleIds;

    public function __construct(
        private readonly DealerKitConfig $config,
        private readonly DealerKitApi $api,
    ) {
    }

    public function enabled(): bool
    {
        return $this->config->stockEnabled();
    }

    public function name(): string
    {
        return 'DealerKit';
    }

    /**
     * Fields exposed on the dealership admin screen.
     */
    public function dealershipAdditionalDataFields(): array
    {
        return [
            'dealerkit-dealer-id' => [
                'type' => 'text',
                'label' => 'Dealer ID within DealerKit',
            ],
        ];
    }

    public function runScheduledImport(bool $fullSync = true): void
    {
        $this->resetStats();
        $this->feedVehicleIds = collect();

        Vehicle::query()
            ->where('stock_provider', self::NAME)
            ->update(['was_recently_synced' => false]);

        $dealerships = $this->loadDealerships();

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

        $dealerships->each(fn (Dealership $dealership) => $this->syncDealership($dealership));

        $this->removeOld();

        Event::dispatch(new StockSyncFinished(self::NAME));
    }

    /**
     * Dealerships in the current tenant that have a DealerKit dealer ID set.
     */
    private function loadDealerships(): Collection
    {
        return Dealership::all()
            ->filter(fn (Dealership $dealership) => !empty($dealership->data['dealerkit-dealer-id'] ?? null));
    }

    private function syncDealership(Dealership $dealership): void
    {
        $dealerId = $dealership->data['dealerkit-dealer-id'];

        $firstPage = $this->fetchPage($dealership, $dealerId, 1);
        if ($firstPage === null) {
            return;
        }

        $this->dispatchBatch($firstPage['data'] ?? [], $dealership->id);

        $lastPage = (int) ($firstPage['meta']['last_page'] ?? 1);
        if ($lastPage <= 1) {
            return;
        }

        collect(range(2, $lastPage))
            ->each(function (int $page) use ($dealership, $dealerId): void {
                $response = $this->fetchPage($dealership, $dealerId, $page);
                if ($response !== null) {
                    $this->dispatchBatch($response['data'] ?? [], $dealership->id);
                }
            });
    }

    private function fetchPage(Dealership $dealership, string $dealerId, int $page): ?array
    {
        try {
            return $this->api->getStockPage($dealerId, $page, self::PER_PAGE);
        } catch (\Exception $exception) {
            Log::error('DealerKit stock fetch failed', [
                'tenant' => tenant('id'),
                'dealership_id' => $dealership->id,
                'dealer_id' => $dealerId,
                'page' => $page,
                'message' => $exception->getMessage(),
            ]);
            return null;
        }
    }

    private function dispatchBatch(array $vehicles, string $dealershipId): void
    {
        if (empty($vehicles)) {
            return;
        }

        $collection = collect($vehicles);
        $feedUuids = $collection->pluck('id')->filter();

        // Record every feed UUID so removeOld() can identify vehicles that have
        // dropped out of the feed once all pages have been dispatched.
        $this->feedVehicleIds = $this->feedVehicleIds->merge($feedUuids)->unique()->values();

        $existingUuids = Vehicle::withTrashed()
            ->where('stock_provider', self::NAME)
            ->whereIn('uuid', $feedUuids)
            ->pluck('uuid');

        // Count created vs updated synchronously. The queued batch jobs do the
        // actual writes in a separate process, so their tallies never reach the
        // sync history row (getImportStats() is read as soon as this method
        // returns). Failures happen in the job and are not tracked here.
        foreach ($feedUuids as $uuid) {
            if ($existingUuids->contains($uuid)) {
                $this->incrementUpdated();
            } else {
                $this->incrementCreated();
            }
        }

        $this->dispatch(new ImportDealerKitVehicleBatch($collection, $dealershipId));
    }

    /**
     * Soft delete DealerKit vehicles that are no longer present in the feed.
     *
     * Guarded by automotive.sync_margin_for_removing: removals only run when a
     * healthy proportion of the existing DealerKit stock was seen in this feed,
     * so a partial or failed feed cannot wipe live stock.
     */
    private function removeOld(): void
    {
        // An empty feed set would make whereNotIn() match every DealerKit row, so
        // never remove when nothing was returned (covers empty or failed feeds).
        if ($this->feedVehicleIds->isEmpty()) {
            return;
        }

        if (!$this->enoughStockInFeed((float) config('automotive.sync_margin_for_removing'))) {
            return;
        }

        Vehicle::query()
            ->where('stock_provider', self::NAME)
            ->whereNotIn('uuid', $this->feedVehicleIds)
            ->chunkById(100, fn (Collection $chunk) => $chunk->each(
                fn (Vehicle $vehicle) => $this->removeWithMedia($vehicle)
            ));
    }

    /**
     * Whether enough of the existing DealerKit stock appeared in the feed to
     * trust removals. Measured against the in-memory feed UUID set accumulated
     * during dispatchBatch(), so it is independent of queue timing (the batch
     * jobs may not have run yet when this is called).
     */
    private function enoughStockInFeed(float $margin): bool
    {
        $total = Vehicle::query()
            ->where('stock_provider', self::NAME)
            ->count();

        if ($total === 0) {
            return false;
        }

        $inFeed = Vehicle::query()
            ->where('stock_provider', self::NAME)
            ->whereIn('uuid', $this->feedVehicleIds)
            ->count();

        return ($inFeed / $total) > $margin;
    }

    private function removeWithMedia(Vehicle $vehicle): void
    {
        $mediaIds = $vehicle->mediaUses()->pluck('media_id');
        $vehicle->mediaUses()->delete();
        $vehicle->delete();
        Media::query()->whereIn('id', $mediaIds)->whereDoesntHave('uses')->delete();
    }
}
