<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\MaxAttemptsExceededException;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Mtc\ContentManager\Contracts\Media as MediaContract;
use Mtc\ContentManager\Facades\Media;
use Mtc\ContentManager\Models\MediaUse;

class ImportImageUrlList implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public $timeout = 0;

    public $tries = 3;

    private Collection $existing;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(
        private readonly Collection $images,
        private readonly Model $owner,
        private readonly array $config = [],
    ) {
        $this->onQueue('bulk-media');
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(): void
    {
        if ($this->shouldSkipAll()) {
            return;
        }
        try {
            $media = $this->importMedia();
            $this->assignUses($media);
            $this->updateMetaData($media);
            $this->removeUnnecessary();
            if ($this->isAutoTraderImageProvider()) {
                // AutoTrader order is the source of truth: the first image of the feed
                // must be the primary, even when a re-sync only changed the order.
                Media::setPrimaryUseByOrder($this->owner);
                // The checksum stored during assignUses() predates the reordering and
                // removals above; refresh it so the next identical feed is a no-op.
                Media::refreshMediaChecksum($this->owner);
            } else {
                Media::ensurePrimaryUse($this->owner);
            }
        } catch (\Exception $exception) {
            Log::error('Image Bulk import failed with exception: ' . $exception->getMessage(), $exception->getTrace());
        }
    }

    public function failed(\Throwable $exception): void
    {
        if (!$exception instanceof MaxAttemptsExceededException) {
            Log::error('Image list import failed with error' . $exception->getMessage(), [
                'exception' => $exception,
                'images' => $this->images,
                'owner' => $this->owner,
                'provider' => $this->config['imageProvider'] ?? null,
            ]);
        }
    }

    private function importMedia(): Collection
    {
        $this->loadExisting();
        return $this->images->map(fn ($image) => $this->importSingleImage($image))->filter();
    }

    private function importSingleImage($image): ?Model
    {
        $existing = $this->alreadyExists($image);
        if ($existing) {
            return $existing;
        }

        try {
            return Media::importImageFromUrl($image, '', $this->config['imageProvider'] ?? null);
        } catch (FileNotFoundException) {
            // File does not exist at url, do not log as error
            return null;
        } catch (\Exception $exception) {
            Log::warning('Failed to import image', [
                'vehicle_id' => $this->owner->id,
                'image_data' => $image,
                'error' => $exception->getMessage(),
            ]);
            return null;
        }
    }

    private function shouldSkipAll(): bool
    {
        return !empty($this->config['onlyWhenNoImages']) && $this->owner->mediaUses()->count() > 0;
    }

    private function alreadyExists($image): ?MediaContract
    {
        if (empty($this->config['imageProvider'])) {
            return null;
        }

        $autoTraderId = $this->config['additionalData'][$image]['auto_trader_id'] ?? null;

        return $this->existing
            ->filter(fn ($item) => $item->source_filename === $image ||
                ($autoTraderId && $item->auto_trader_id === $autoTraderId))
            ->first();
    }

    private function loadExisting(): void
    {
        if (!isset($this->existing)) {
            $this->existing = \Mtc\MercuryDataModels\Media::query()
                ->with('uses')
                ->where('image_provider', $this->config['imageProvider'] ?? null)
                ->when(
                    ($this->config['imageProvider'] ?? null) === 'auto-trader',
                    function ($query) {
                        $autoTraderIds = collect($this->config['additionalData'])
                            ->pluck('auto_trader_id')
                            ->filter()
                            ->toArray();

                        $query->where(fn ($q) => $q
                            ->whereIn('source_filename', $this->images)
                            ->orWhereIn('auto_trader_id', $autoTraderIds));
                    },
                    fn ($query) => $query->whereIn('source_filename', $this->images)
                )
                ->get();
        }
    }

    private function assignUses(Collection $media): void
    {
        $mediaIds = $media
            ->filter(fn ($entry) => $this->shouldTriggerEntryUpdate($entry))
            ->pluck('id')
            ->toArray();

        // For AutoTrader the primary is derived from the final feed order at the end of
        // handle(), so the batch must not elect its own here - a newly added mid-list
        // image is first in the batch and would steal the flag from the real first image.
        $updatePrimary = !$this->isAutoTraderImageProvider()
            && ($this->config['updatePrimary'] ?? true);

        $meta = array_merge(['primary' => $updatePrimary], $this->config['meta'] ?? []);

        Media::setUsesForModel($mediaIds, $this->owner, $meta, false);
    }

    private function shouldTriggerEntryUpdate(MediaContract $existing): bool
    {
        // If flag is true we always want to update
        if (!empty($this->config['updateAssigned'])) {
            return true;
        }

        // Otherwise we only want to assign if the record is not assigned to the model
        return $existing->uses
                ->where('owner_type', $this->owner->getMorphClass())
                ->where('owner_id', $this->owner->id)
                ->count() == 0;
    }

    protected function updateMetaData(Collection $media): void
    {
        $this->updateImageOrder($media);

        $media->each(function (MediaContract $media) {
            $additionalData = [];
            if ($this->isAutoTraderImageProvider() && empty($media->source_filename) && $media->auto_trader_id) {
                $additionalData = collect($this->config['additionalData'])
                    ->first(fn ($item) => $item['auto_trader_id'] === $media->auto_trader_id);

                if (!$additionalData) {
                    return;
                }
            } elseif (
                !empty($media->source_filename) &&
                !empty($this->config['additionalData'][$media->source_filename])
            ) {
                $additionalData = $this->config['additionalData'][$media->source_filename];
            } else {
                return;
            }

            $media->update($additionalData);
            $media->uses->each(fn (MediaUse $mediaUse) => $mediaUse->update($additionalData));
        });
    }

    private function updateImageOrder(Collection $media): void
    {
        if (!empty($this->owner)) {
            Media::setUseOrdering($media->pluck('id')->flip()->toArray(), $this->owner);
        }
    }

    private function removeUnnecessary(): void
    {
        if (
            !empty($this->getImageProvider())
            && !empty($this->owner)
            && empty($this->config['doNotRemoveOthers'])
        ) {
            MediaUse::query()->whereIn('id', $this->getImagesToRemove())->delete();
        }
    }

    private function getImagesToRemove(): Collection
    {
        $media_uses = $this->owner->mediaUses()
            ->with('media')
            ->whereHas('media', fn ($query) => $query
                ->when(
                    $this->isAutoTraderImageProvider(),
                    fn ($query) => $query->whereNotNull('auto_trader_id'),
                    fn ($query) => $query->where('image_provider', $this->getImageProvider())
                ))
            ->get();

        $not_in_list = $media_uses
            ->filter(fn ($mediaUse) => (
                $this->getImageProvider() !== 'auto-trader'
                    ? !in_array($mediaUse->media->source_filename, $this->images->toArray())
                    : !in_array(
                        $mediaUse->media->auto_trader_id,
                        collect($this->config['additionalData'])
                            ->pluck('auto_trader_id')
                            ->filter()
                            ->toArray()
                    )
            ))
            ->pluck('id');

        $duplicates = $media_uses->groupBy(fn ($mediaUse) =>
        $this->isAutoTraderImageProvider()
            ? $mediaUse->media->auto_trader_id
            : $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 getImageProvider()
    {
        return $this->config['imageProvider'] ?? null;
    }

    private function isAutoTraderImageProvider(): bool
    {
        return $this->getImageProvider() === 'auto-trader';
    }
}
