<?php

namespace App;

use App\Contracts\ExternalMediaUrlProvider;
use App\Http\Requests\CreateCarConfigurator;
use App\Jobs\ImportConfiguratorImages;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Mtc\ContentManager\Facades\Media;
use Mtc\MercuryDataModels\CarConfiguratorModel;
use Mtc\MercuryDataModels\CarConfiguratorRestriction;
use Mtc\MercuryDataModels\MediaUse;

class CarConfiguratorRepository implements ExternalMediaUrlProvider
{
    use DispatchesJobs;
    use SavesSeoData;

    private static array $new_id_map = [];

    public function create(CreateCarConfigurator $request): CarConfiguratorModel
    {
        /** @var CarConfiguratorModel $model */
        $model = CarConfiguratorModel::query()
            ->create([
                'name' => $request->input('name'),
                'make_id' => $request->input('make_id'),
                'model_id' => $request->input('model_id'),
            ]);

        foreach (CarConfiguratorModel::$relationship_models as $order => $relationship) {
            if ($relationship === 'custom') {
                continue;
            }
            $model->sections()->create([
                'name' => ucwords((string) $relationship),
                'custom' => false,
                'order' => $order,
                'active' => $this->getSectionDefaults($relationship, 'active'),
                'data' => $this->getSectionDefaults($relationship, 'data')
            ]);
        }

        return $model;
    }

    public function update(Request $request, CarConfiguratorModel $model)
    {
        $request->merge(['data' => $request->input('content')]);
        $this->saveSeo($model, $request->input('seo', []));
        $model->update($request->input());
        $relationships = [
            'sections',
            'editions',
            'trims',
            'engines',
            'colours',
            'wheels',
            'interiors',
            'packages',
            'extras',
            'custom',
        ];

        $this->attachContentMedia($model);

        foreach ($relationships as $relationship) {
            $this->syncRelationship($relationship, $request->input($relationship, []), $model);
        }
    }

    public function attachContentMedia(Model $model): void
    {
        $mediaFiles = collect();
        $this->getMediaFilesFromStructure(collect($model->data ?? []), $mediaFiles);
        Media::setUsesForModel($mediaFiles->flatten()->unique()->toArray(), $model, [], false);
    }

    private function getMediaFilesFromStructure(Collection $group, Collection $mediaFiles): void
    {
        $group->each(function ($content) use ($mediaFiles) {
            if (($content['field_type'] ?? '') === 'image' || ($content['fieldId'] ?? '') === 'image') {
                $mediaFiles->push($content['content'] ?? []);
            }
            if (!empty($content['children'])) {
                $this->getMediaFilesFromStructure(collect($content['children']), $mediaFiles);
            }
        });
    }

    public function remove(CarConfiguratorModel $model)
    {
        $relationships = [
            'sections',
            'editions',
            'trims',
            'engines',
            'colours',
            'wheels',
            'interiors',
            'packages',
            'extras',
            'custom',
        ];
        foreach ($relationships as $relationship) {
            $model->$relationship()->delete();
        }
        $model->mediaUses()->with('media.uses')->get()
            ->each(function ($use) {
                $otherUses = $use->media->uses->count() > 1;
                if (!$otherUses) {
                    Media::destroyMedia([$use->id]);
                }
                $use->delete();
            });
        $model->delete();
    }

    private function syncRelationship(string $relationship, array $input, CarConfiguratorModel $carConfiguration): void
    {
        $query = match ($relationship) {
            'sections' => $carConfiguration->sections(),
            'editions' => $carConfiguration->editions(),
            'trims' => $carConfiguration->trims(),
            'engines' => $carConfiguration->engines(),
            'colours' => $carConfiguration->colours(),
            'wheels' => $carConfiguration->wheels(),
            'interiors' => $carConfiguration->interiors(),
            'packages' => $carConfiguration->packages(),
            'extras' => $carConfiguration->extras(),
            'custom' => $carConfiguration->custom(),
            default => throw new \Exception('Unrecognized relationship for configurator'),
        };

        if (in_array($relationship, ['packages', 'extras'])) {
            $this->syncBelongsToMany($relationship, $query, $input, $carConfiguration);
            return;
        }

        $existing = $query->get();
        $ids = collect($input)->pluck('id');
        // Update
        $existing->filter(fn(Model $entry) => $ids->search($entry->id) !== false)
            ->each(fn(Model $entry, $order) => $this->updateRelated(
                $entry,
                $input,
                $relationship,
                $ids->search($entry->id)
            ));
        // Remove discarded
        $existing->reject(fn(Model $entry) => $ids->search($entry->id) !== false)
            ->each(fn(Model $entry) => $entry->delete());
        // Add new
        collect($input)
            ->filter(fn(array $row) => $existing->where('id', $row['id'])->isEmpty())
            ->each(fn($entry, $order) => $this->addRelated($entry, $query, $relationship, $order));
    }

    private function updateRelated(Model $entry, array $allInput, string $relationship, int $order): void
    {
        $data = collect($allInput)->filter(fn($row) => $row['id'] == $entry->id)->first();
        $data = $this->adjustInput($data, $relationship, $order);
        $entry->update($data);
        if (method_exists($entry, 'mediaUses')) {
            Media::setUsesForModel($data['media'] ?? [], $entry);
            if (!empty($data['media'])) {
                Media::setUseOrdering(array_flip($data['media']), $entry);
            }
        }
        if (method_exists($entry, 'restrictions')) {
            $this->syncRestrictions($data, $entry);
        }
    }

    private function addRelated(array $entry, HasMany $query, string $relationship, int $order): void
    {
        $entry = $this->adjustInput($entry, $relationship, $order);
        $object = $query->create($entry);
        self::$new_id_map[$relationship][$entry['id']] = $object->id;
        if (method_exists($object, 'mediaUses')) {
            Media::setUsesForModel($data['media'] ?? [], $object);
            Media::setUseOrdering($data['media'] ?? [], $object);
        }
        if (method_exists($object, 'restrictions')) {
            $this->syncRestrictions($entry, $object);
        }
    }

    private function getRestrictionId($restriction): ?int
    {
        if (is_numeric($restriction['restriction_id'])) {
            return $restriction['restriction_id'];
        }

        return self::$new_id_map[$restriction['restriction_type']][$restriction['restriction_id']] ?? null;
    }

    private function adjustInput(array $entry, string $relationship, int $order): array
    {
        $entry['order'] = $order;

        if ($relationship === 'custom' && !is_numeric($entry['section_id'])) {
            $entry['section_id'] = self::$new_id_map['custom'][$entry['section_id']] ?? null;
        }

        return $entry;
    }

    private function syncRestrictions(array $entry, Model $object)
    {
        $object->restrictions()->delete();
        collect($entry['restrictions'] ?? [])
            ->filter(fn($restriction) => !empty($restriction['restriction_id']))
            ->each(fn($restriction) => $object->restrictions()->create([
                'model_id' => $object->model_id,
                'restriction_id' => $this->getRestrictionId($restriction),
                'restriction_type' => $restriction['restriction_type'],
                'condition' => $restriction['condition'],
            ]));
    }

    private function syncBelongsToMany(
        string $relationship,
        BelongsToMany $query,
        array $input,
        CarConfiguratorModel $config
    ): void {
        $related_column = $relationship === 'packages' ? 'package_id' : 'extra_id';
        $data = collect($input)
            ->keyBy($related_column)
            ->map(fn($row) => ['price' => $row['price'] ?? null])
            ->toArray();
        $query->sync($data);

        // Restrictions
        $config->restrictions()->where('functionality_type', $relationship)->delete();
        collect($input)->keyBy($related_column)
            ->filter(fn($entry) => !empty($entry['restrictions']))
            ->each(fn($entry, $id) => collect($entry['restrictions'])
                ->each(fn($restriction) => CarConfiguratorRestriction::query()
                    ->create([
                        'model_id' => $config->id,
                        'functionality_type' => $relationship,
                        'functionality_id' => $id,
                        'restriction_id' => $this->getRestrictionId($restriction),
                        'restriction_type' => $restriction['restriction_type'],
                        'condition' => $restriction['condition'],
                    ])));
    }

    private function getSectionDefaults(string $relationship, string $field): mixed
    {
        return match ($field) {
            'active' => $relationship !== 'editions',
            'data' => match ($relationship) {
                'editions',
                'trims',
                'colours',
                'wheels' => ['exterior' => true, 'interior' => false],
                'interiors' => ['exterior' => false, 'interior' => true],
                default => null,
            },
            default => null,
        };
    }

    public function saveMedia(CarConfiguratorModel $configuration, string $flag, array $media, array $wheelImages): void
    {
        $configuration->mediaUses()
            ->where('flags', 'like', '%"' . $flag . '"%')
            ->get()
            ->each(fn(MediaUse $use) => $this->removeFlagMediaUsage($flag, $use, $media));

        $existing = $configuration->mediaUses()
            ->whereIn('media_id', $media)
            ->get();

        $existing->each(fn(MediaUse $use) => Media::setUsesForModel(
            [$use->media_id],
            $configuration,
            [
                'flags' => array_merge([$flag => 'true'], $use->flags ?? []),
                'exterior' => in_array($use->media_id, $wheelImages),
            ],
            false
        ))
            ->each(fn(MediaUse $use) => $use->update(['exterior' => in_array($use->media_id, $wheelImages)]));

        Media::setUseOrdering(array_flip($media), $configuration);
        $existing_ids = $existing->pluck('media_id')->toArray();
        $additions = collect($media)
            ->reject(fn($id) => in_array($id, $existing_ids))
            ->toArray();
        if (!empty($additions)) {
            Media::setUsesForModel($additions, $configuration, ['flags' => [$flag => 'true']], false);
        }
    }


    public function importMedia(CarConfiguratorModel $configurator, UploadedFile $file): void
    {
        $data = $this->getContentFromFile($file);
        $this->dispatch(new ImportConfiguratorImages($configurator, $data));
    }

    private function getContentFromFile(UploadedFile $file): array
    {
        if ($file->getClientOriginalExtension() === 'json') {
            return json_decode($file->getContent(), true);
        }
        return [];
    }

    private function removeFlagMediaUsage(string $flag, MediaUse $use, array $media): void
    {
        // In list to be saved
        if (in_array($use->media_id, $media)) {
            return;
        }

        // Not in list, needs to be removed
        $flags = $use->flags ?? [];
        unset($flags[$flag]);

        // If no other flags on use, then remove, otherwise update without this use
        if (empty($flags)) {
            $use->delete();
        } else {
            $use->update(['flags' => $flags]);
        }
    }

    public function url(\Mtc\ContentManager\Contracts\Media $media, string $size): string
    {
        $dimensions = explode('x', $size);

        return str_replace('w=600', 'w=' . $dimensions[0], $media->src);
    }
}
