<?php

namespace App\Traits;

use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Mtc\ContentManager\Contracts\Content;
use Mtc\ContentManager\Contracts\ContentElement as ContentElementContract;
use Mtc\ContentManager\Contracts\ContentElementField;
use Mtc\ContentManager\Contracts\TemplateElement;
use Mtc\ContentManager\Facades\Media;
use Mtc\ContentManager\Models\VersionContent;
use Mtc\MercuryDataModels\ContentElement;
use Mtc\MercuryDataModels\Contracts\ContentHistoryModel;
use Mtc\MercuryDataModels\Contracts\ContentModel;
use Mtc\MercuryDataModels\Contracts\ModelWithContent;
use Ramsey\Uuid\Uuid;

trait SavesContent
{
    abstract protected function elementParentColumnName(): string;
    abstract protected function contentOwnerType(): string;
    abstract protected function getQuery(): Builder;

    public function isContentElementInSync(ModelWithContent $model, ContentElementContract $element): bool
    {
        $elementsInPage = $model->allContent()->where('element_id', $element->id)->get();
        return $this->pageElementsMatchingContentElement($elementsInPage, $element)
            ->count() == $elementsInPage->count();
    }

    public function syncContentWithTemplate(int $model_id, ?ContentElementContract $element = null): void
    {
        if ($element) {
            $this->syncSpecificElementOnPage($model_id, $element);
            return;
        }

        /** @var ModelWithContent $temp_model */
        $temp_model = $this->getQuery()->find($model_id);
        $this->elementsOnPageOutOfSync($temp_model)
            ->each(fn(ContentElement $element) => $this->syncSpecificElementOnPage($model_id, $element));

        $model = $this->loadModelWithSyncRelationships($model_id);
        if (empty($model->template_id)) {
            return;
        }

        $model->template->elements
            ->each(function (TemplateElement $template_element, $order) use ($model) {
                $on_model_content = $this->ensureTemplateElementIsInModelContent($model, $template_element, $order);
                if ($template_element->element) {
                    $template_element->element->fields
                        ->each(fn(ContentElementField $field, $order) => $this->ensureElementFieldIsOnModelContent(
                            $on_model_content,
                            $field,
                            $model,
                            $order
                        ));
                }
            });

        $this->dropTemplateContent($this->getElementsFromDifferentTemplate($model));
        $this->dropTemplateContent($this->getOrphanedElements($model));
    }
    private function elementsOnPageOutOfSync(ModelWithContent $model): Collection
    {
        return ContentElement::query()
            ->whereIn('id', $model->allContent()->whereNotNull('element_id')->pluck('element_id'))
            ->get()
            ->reject(fn(ContentElement $element) => $this->isContentElementInSync($model, $element));
    }

    protected function syncSpecificElementOnPage(int $owner_id, ContentElement $element): void
    {
        /** @var ?ModelWithContent $owner */
        $owner = $this->getQuery()
            ->with('allContent.subContent')
            ->find($owner_id);
        if (!$owner) {
            return;
        }

        $owner->allContent->where('element_id', $element->id)
            ->each(fn(Content $pageContent, $index) => $element->fields
                ->each(fn(ContentElementField $field) => $this->ensureElementFieldIsOnModelContent(
                    $pageContent,
                    $field,
                    $owner,
                    $index
                )))
            ->each(fn($pageContent, $index) => $this->dropRemovedFieldEntriesFromContent(
                $pageContent,
                $element->fields->pluck('slug')
            ));
    }

    protected function dropRemovedFieldEntriesFromContent(Model $parent_content, Collection $field_slugs): void
    {
        $parent_content->subContent
            ->whereNotIn('slug', $field_slugs)
            ->each(fn($contentEntry) => $contentEntry->delete());
    }

    public function restoreVersion(ModelWithContent $model, ContentHistoryModel $version): bool
    {
        try {
            DB::beginTransaction();
            collect($version->allContent)
                ->each(fn($element, $index) => $this->saveContentElement($model, $element, $index));

            $model->update($version->data);
            $version->is_active = true;
            $version->pending_review = false;
            $version->request_changes = false;
            $version->save();
            DB::commit();

            return true;
        } catch (Exception $exception) {
            Log::error('Failed to restore new vehicle version', [
                'car' => $model,
                'version' => $version,
                'tenant' => tenant()->id,
                'error' => $exception->getMessage()
            ]);
            return false;
        }
    }

    protected function ensureTemplateElementIsInModelContent(
        ModelWithContent $owner,
        TemplateElement $element,
        int $order
    ): Model {
        $template_content = $owner->content->where('template_element_id', $element->id)->first();
        if (!$template_content) {
            $template_content = $owner->content()->create($this->templateElementToContent($element, $order));
        }
        return $template_content;
    }

    protected function ensureElementFieldIsOnModelContent(
        Model $parent_content,
        ContentElementField $field,
        ModelWithContent $owner,
        int $index
    ): void {
        $field_entries_on_model = $parent_content->subContent->where('slug', $field->slug);
        if ($field_entries_on_model->isEmpty()) {
            $field_entries_on_model = [$this->addElementFieldToModelContent($parent_content, $field, $index, $owner)];
        }

        foreach ($field_entries_on_model as $entry) {
            $entry->update([
                'order' => $index,
                'name' => $field->name,
            ]);

            $entry->subContent->whereNotIn('slug', $field->childElement?->fields->pluck('slug'))
                ->each(fn($entry_to_remove) => $entry_to_remove->delete());

            $field->childElement?->fields
                ->each(fn(ContentElementField $childField, $order) => $this->ensureElementFieldIsOnModelContent(
                    $entry,
                    $childField,
                    $owner,
                    $order
                ));
        }
    }

    protected function templateElementToContent(TemplateElement $template, int $order): array
    {
        $data = $template->toArray();
        $data['template_element_id'] = $template->id;
        if ($template->element_id) {
            $data['data'] = $template->element->data ?? [];
            if (!empty($template->element->icon)) {
                $data['data']['icon'] = $template->element->icon;
            }
        }

        if (empty($data['slug'])) {
            if (!empty($template->data['slug'])) {
                $data['slug'] = $template->data['slug'];
            }
            if (!empty($template->data['fieldId'])) {
                $data['slug'] = $template->data['fieldId'];
            }
        }
        $data['order'] = $order;
        unset($data['id']);
        return $data;
    }

    protected function addElementFieldToModelContent(
        Model $model_content_entry,
        ContentElementField $field,
        int $order,
        ModelWithContent $owner
    ): ?Model {
        return $model_content_entry->subContent()
            ->create([
                $owner->getOwnerColumn() => $owner->id,
                'element_id' => $field->child_element_id,
                'parent_id' => $model_content_entry->id,
                'slug' => $field->slug,
                'name' => $field->name,
                'data' => $this->getContentElementFieldData($field),
                'order' => $order
            ]);
    }

    /**
     * @param int $model_id
     * @return ModelWithContent|Model
     */
    protected function loadModelWithSyncRelationships(int $model_id): ModelWithContent
    {
        return $this->getQuery()
            ->with([
                'content.subContent.subContent.subContent.subContent',
                'template.elements.element.fields.childElement.fields.childElement'
                . '.fields.childElement.fields.childElement',
            ])
            ->findOrFail($model_id);
    }

    /**
     * Save content element against page or its content element
     *
     * @param ModelWithContent|ContentModel $parent
     * @param array $element
     * @param int $order
     */
    protected function saveContentElement(ModelWithContent|ContentModel $parent, array $element, int $order): void
    {
        $column = $this->elementParentColumnName();
        $element['data'] = collect($element)->except([
            'id',
            'name',
            'content',
            'parent_id',
            $column,
            'global_content_id',
            'element_id',
            'children',
        ]);

        $element['order'] = $order;

        if (empty($element[$column])) {
            $element[$column] = $parent instanceof ModelWithContent
                ? $parent->id
                : $parent->{$column};
        }

        /** @var ContentModel $entry */
        $entry = $parent instanceof ModelWithContent
            ? $parent->content()->updateOrCreate(['id' => $element['id']], $element)
            : $parent->subContent()->updateOrCreate(['id' => $element['id']], $element);

        if (($element['fieldId'] ?? '') === 'image') {
            $this->syncContentElementMedia($element['content'], $entry);
        }
        collect($element['children'] ?? [])->each(
            fn($content, $index) => $this->saveContentElement($entry, $content, $index)
        );
    }

    /**
     * Set media elements on Content
     *
     * @param array $media_entries
     * @param ContentModel|VersionContent $entry
     * @return void
     */
    protected function syncContentElementMedia(array $media_entries, ContentModel|VersionContent $entry): void
    {
        $ids = collect($media_entries)
            ->map(fn($entry) => is_numeric($entry) ? $entry : $entry['id']);
        $to_remove = $entry->mediaUses
            ->reject(fn($media_use) => $ids->search($media_use['media_id']) !== false)
            ->pluck('id');

        if ($to_remove->isNotEmpty()) {
            $entry->mediaUses()->whereIn('id', $to_remove)->delete();
        }

        $to_add = $ids->filter(fn($id) => $entry->mediaUses->pluck('media_id')->search($id) === false);

        if ($to_add->isNotEmpty()) {
            Media::setUsesForModel($ids->toArray(), $entry);
        }
    }

    protected function copyContent(ModelWithContent|ContentModel $parent, $content): void
    {
        /** @var ContentModel $entry */
        $entry = $parent instanceof ModelWithContent
            ? $parent->content()->create($content->toArray())
            : $parent->subContent()->create($content->toArray());

        if (!empty($content->subContent)) {
            $content->subContent->each(fn($subContent) => $this->copyContent($entry, $subContent));
        }
    }

    protected function getOrphanedElements(ModelWithContent $model): Collection
    {
        return $model->content()
            ->whereDoesntHave('templateElement')
            ->whereNotNull('template_element_id')
            ->get();
    }

    public function isContentOutOfSync(ModelWithContent $model): bool
    {
        if (empty($model->template_id)) {
            return false;
        }
        $model->load([
            'template.elements.element.fields.childElement.fields.childElement'
            . '.fields.childElement.fields.childElement'
        ]);

        return $this->getMissingStructure($model)->isNotEmpty()
            || $this->elementsOnPageOutOfSync($model)->isNotEmpty()
            || $this->getElementsFromDifferentTemplate($model)->isNotEmpty()
            || $this->getOrphanedElements($model)->isNotEmpty();
    }

    private function getElementsFromDifferentTemplate(ModelWithContent $model): Collection
    {
        if (empty($model->template_id)) {
            return collect([]);
        }

        return $model->content()
            ->whereNotNull('template_element_id')
            ->whereHas('templateElement', fn ($query) => $query->where('template_id', '!=', $model->template_id))
            ->get();
    }


    protected function dropTemplateContent(Collection $structure): void
    {
        $structure->each(fn($content) => $content->delete());
    }

    protected function setContentDataFromFieldChild(ContentModel $subContent, ContentElementField $field): void
    {
        $data = $field->childElement->data ?? [];
        if (!empty($field->childElement->icon)) {
            $data['icon'] = $field->childElement->icon;
        }
        $subContent->update([
            'data' => $data,
        ]);
    }

    /**
     * Check if template element exists on page
     *
     * @param TemplateElement $element
     * @param ModelWithContent $model
     * @return bool
     */
    protected function isTemplateElementMissingInModelContent(TemplateElement $element, ModelWithContent $model): bool
    {
        return  $model->content->where('template_element_id', $element->id)->isEmpty();
    }

    protected function areElementFieldsMissingInContent(TemplateElement $templateElement, ModelWithContent $model): bool
    {
        if (!empty($templateElement->global_content_id)) {
            return false;
        }

        return $templateElement->element?->fields
            ->filter(
                fn (ContentElementField $field) => $this->isFieldMissingFromContent($field, $model, $templateElement)
            )->isNotEmpty() ?? false;
    }

    protected function isFieldMissingFromContent(
        ContentElementField $field,
        ModelWithContent $model,
        TemplateElement $templateElement
    ): bool {
        return $model->content
            ->where('template_element_id', $templateElement->id)
            ->first()
            ?->subContent->where('slug', $field->slug)
            ->isEmpty() ?? true;
    }

    protected function syncElementToModel(ModelWithContent $model, ContentElement $element)
    {
    }

    protected function mergeContentStructure(ModelWithContent $model, Collection $structure): void
    {
        $structure->each(function ($missingElement) use ($model) {
            $elementData = $missingElement->toArray();
            $elementData['slug'] = $missingElement->global_content_id
                ? $missingElement->globalContent->slug
                : $missingElement->element?->slug;

            /** @var ContentModel $contentEntry */
            $contentEntry =  $model->content()
                ->updateOrCreate([
                    'template_element_id' => $missingElement->id,
                ], $elementData);

            // This is a content element that has fields underneath it
            if ($missingElement->element) {
                $this->nestContentElementsForContent($contentEntry, $missingElement->element, true);
            }
        });
    }

    protected function nestContentElementsForContent(
        ContentModel $contentEntry,
        ContentElement $contentElement,
        bool $update = false
    ): void {
        $contentElement->fields
            ->each(fn (ContentElementField $field, $index) => $this->addContentFieldToContentModel(
                $contentEntry,
                $field,
                $index,
                $update
            ));
    }

    private function addContentFieldToContentModel(
        ContentModel $contentEntry,
        ContentElementField $field,
        int $order,
        bool $update = false
    ): void {
        $column = $this->elementParentColumnName();
        /** @var ContentModel $subContent */

        $subContent = $update
            ? $contentEntry->subContent()
                ->updateOrCreate([
                    $column => $contentEntry->{$column},
                    'element_id' => $field->child_element_id,
                    'slug' => $field->slug,
                    'parent_id' => $contentEntry->id,
                ], [
                    'name' => $field->name,
                    'data' => $this->getContentElementFieldData($field),
                    'order' => $order
                ])
            : $contentEntry->subContent()->create([
                $column => $contentEntry->{$column},
                'element_id' => $field->child_element_id,
                'parent_id' => $contentEntry->id,
                'slug' => $field->slug,
                'name' => $field->name,
                'data' => $this->getContentElementFieldData($field),
                'order' => $order
            ]);

        if ($field->childElement) {
            $this->nestContentElementsForContent($subContent, $field->childElement);

            if (empty($field->data)) {
                $this->setContentDataFromFieldChild($subContent, $field);
            }
        }
    }

    /**
     * @param ModelWithContent $model
     * @return Collection
     */
    protected function getMissingStructure(ModelWithContent $model): Collection
    {
        if (empty($model->template_id)) {
            return collect();
        }

        return $model->template?->elements
            ->filter(fn ($element) => $this->isTemplateElementMissingInModelContent($element, $model)
                || $this->areElementFieldsMissingInContent($element, $model)) ?? collect();
    }

    /**
     * Save a new version of the offer content
     *
     * @param ModelWithContent $model
     * @param array $content
     * @param int $author
     * @param false $is_active
     */
    protected function saveVersion(ModelWithContent $model, array $content, int $author, bool $is_active = false): void
    {
        if ($this->hasContentChanged($model, $content) === false) {
            return;
        }

        /** @var ContentHistoryModel $version */
        $version = $model->versions()
            ->create([
                'uuid' => Uuid::uuid4(),
                'author_id' => $author,
                'is_active' => $is_active,
                'data' => $model->only(self::$dataFields),
            ]);
        collect($content)
            ->each(fn($element) => $this->saveVersionContentElement($version, $element));
    }

    /**
     * Check if content has changed
     *
     * @param ModelWithContent $model
     * @param array $current_content
     * @param int|null $version_id
     * @return bool
     */
    protected function hasContentChanged(ModelWithContent $model, array $current_content, int $version_id = null): bool
    {
        $version = $model->versions()
            ->when(
                $version_id,
                fn($query) => $query->where('id', $version_id),
                fn($query) => $query->where('is_active', 1)
            )
            ->first();

        return ($version->content ?? []) !== $current_content
            || $model->isDirty(self::$dataFields);
    }

    protected function saveVersionContentElement(ContentHistoryModel|VersionContent $parent, array $element): void
    {
        $element['owner_type'] = $this->contentOwnerType();
        if (empty($element['owner_id'])) {
            $element['owner_id'] = $parent instanceof ContentHistoryModel
                ? $parent->id
                : $parent->owner_id;
        }

        /** @var VersionContent $entry */
        $entry = $parent instanceof ContentHistoryModel
            ? $parent->content()->updateOrCreate(['id' => $element['id']], $element)
            : $parent->subContent()->updateOrCreate(['id' => $element['id']], $element);


        if (($element['fieldId'] ?? '') === 'image') {
            $this->syncContentElementMedia($element['content'], $entry);
        }
        collect($element['children'] ?? [])->each(fn($content) => $this->saveVersionContentElement($entry, $content));
    }

    protected function dropOrphanedContent(ModelWithContent $model, array $treeIds): void
    {
        $model->allContent()
            ->whereNotIn('id', $treeIds)
            ->delete();
    }

    protected function getFlatContentElementIds(array|null $content): array
    {
        $ids = collect([]);
        collect($content)->each(function ($entry) use ($ids) {
            if (is_numeric($entry['id'])) {
                $ids->push($entry['id']);
            }
            if (!empty($entry['children'])) {
                $ids->push($this->getFlatContentElementIds($entry['children']));
            }
        });

        return $ids->flatten()->toArray();
    }

    private function pageElementsMatchingContentElement(Collection $elementsInPage, ContentElement $element): Collection
    {
        return $elementsInPage
            // TODO: issue with parent element overwriting current element data values so disabling this check now
            // ->reject(fn(Content $content) => $this->pageElementDoesNotMatchTemplateData($content, $element))
            ->reject(fn(Content $content) => $this->pageElementFieldsDoNotMatchTemplate($content, $element));
    }

    private function pageElementDoesNotMatchTemplateData(Content $inPageContent, ContentElement $element): bool
    {
        return collect($inPageContent->data ?? [])
            ->only(array_keys($element->data ?? []))
            ->reject(fn($data, $key) => $data == $element->data[$key])
            ->isNotEmpty();
    }

    private function pageElementFieldsDoNotMatchTemplate(Content $inPageContent, ContentElement $element): bool
    {
        $missing_field = $element->fields
            ->filter(fn($field) => $inPageContent->subContent?->where('slug', $field->slug)->isEmpty())
            ->isNotEmpty();

        $excess_field = $inPageContent->subContent
            ->pluck('slug')
            ->diff($element->fields->pluck('slug'))
            ->isNotEmpty();

        return $missing_field || $excess_field;
    }
}
