<?php

namespace App\Jobs;

use App\Events\VehicleUpdated;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Queue\ShouldBeUnique;
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\App;
use Illuminate\Support\Facades\Log;
use Mtc\ContentManager\Contracts\Media as MediaContract;
use Mtc\ContentManager\Facades\Media;
use Mtc\ContentManager\Models\MediaUse;
use Mtc\MercuryDataModels\Vehicle;

class ImportImagesFromUrlList implements ShouldQueue, ShouldBeUnique
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public $timeout = 0;

    public $tries = 3;

    private Collection $existing;
    private array $additionalData = [];

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(
        private Collection $images,
        private readonly Model $owner,
        private readonly bool $onlyWhenNoImages = false,
        private readonly ?string $imageProvider = null,
        private readonly bool $removeOthers = false,
        private readonly bool $updateAssigned = true,
        private readonly bool $updatePrimary = true,
    ) {
        $this->onQueue('bulk-media');
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(): void
    {
        if ($this->shouldSkip()) {
            return;
        }
        try {
            $this->images = $this->images->map(fn($image) => $this->encodeUrl($image));
            $mediaIds = $this->importMedia();
            Media::setUsesForModel($mediaIds, $this->owner, ['primary' => $this->updatePrimary], false);
            if ($this->removeOthers && $this->imageProvider && $this->owner) {
                MediaUse::query()->whereIn('id', $this->getImagesToRemove())->delete();
            }
            $this->updateAdditionalData($mediaIds);
            $this->dispatchEvents();
        } 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->imageProvider,
            ]);
        }
    }

    /**
     * Import media records for
     * @return array
     */
    private function importMedia(): array
    {
        $this->loadExisting();
        return $this->images
            ->map(function ($image) {
                $existing = $this->alreadyExists($image);
                if ($existing) {
                    return $this->shouldTriggerEntryUpdate($existing) ? $existing : null;
                }
                try {
                    $media = Media::importImageFromUrl($image, '', $this->imageProvider);
                    return $media;
                } 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;
                }
            })
            ->filter()
            ->pluck('id')
            ->toArray();
    }

    private function encodeUrl(string $url): string
    {
        $parts = parse_url($url);

        if (isset($parts['path'])) {
            $parts['path'] = implode('/', array_map(rawurlencode(...), explode('/', $parts['path'])));
        }

        return (isset($parts['scheme']) ? $parts['scheme'] . '://' : '')
            . ($parts['host'] ?? '')
            . ($parts['path'] ?? '')
            . (isset($parts['query']) ? '?' . $parts['query'] : '');
    }

    private function shouldSkip(): bool
    {
        return $this->onlyWhenNoImages && $this->owner->mediaUses()->count() > 0;
    }

    private function alreadyExists($image): ?Model
    {
        if (empty($this->imageProvider)) {
            return null;
        }

        return $this->existing
            ->where('source_filename', $image)
            ->first();
    }

    private function getImagesToRemove(): Collection
    {
        $image_array = $this->images->toArray();
        $media_uses = $this->owner->mediaUses()->with('media')
            ->whereHas('media', fn($query) => $query->where('image_provider', $this->imageProvider))
            ->get();

        $not_in_list = $media_uses
            ->filter(fn($mediaUse) => !in_array($mediaUse->media->source_filename, $image_array))
            ->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 shouldTriggerEntryUpdate(MediaContract $existing): bool
    {
        // If flag is true we always want to update
        if ($this->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;
    }

    private function loadExisting(): void
    {
        $this->existing = \Mtc\MercuryDataModels\Media::query()
            ->with('uses')
            ->where('image_provider', $this->imageProvider)
            ->whereIn('source_filename', $this->images)
            ->get();
    }

    public function setAdditionalData(array $additionalData = []): static
    {
        $this->additionalData = $additionalData;
        return $this;
    }

    protected function updateAdditionalData($mediaIds = []): void
    {
        $mediaModel = App::make(MediaContract::class);
        $media = $mediaModel->with('uses')->whereIn('id', $mediaIds)->get();

        $media->each(function (MediaContract $media) {
            if (!empty($this->additionalData[$media->source_filename])) {
                $additionalData = $this->additionalData[$media->source_filename];
                $media->fill($additionalData);
                $media->save();

                $media->uses->each(function (MediaUse $mediaUse) use ($additionalData) {
                    $mediaUse->fill($additionalData);
                    $mediaUse->save();
                });
            }
        });
    }

    public function uniqueFor(): int
    {
        // 4h
        return 14400;
    }

    public function uniqueId(): string
    {
        return implode('-', array_filter([
            tenant('id'),
            $this->imageProvider,
            $this->owner->id,
            $this->owner->getMorphClass(),
            $this->images->count(),
        ]));
    }

    private function dispatchEvents(): void
    {
        if ($this->owner instanceof Vehicle) {
            VehicleUpdated::dispatch($this->owner);
        }
    }
}
