<?php

namespace App;

use App\Contracts\AbleToSyncContentElements;
use App\Contracts\InteractsWithContentSync;
use App\Facades\Settings;
use App\Notifications\PageVersionMarkedChangesRequested;
use App\Notifications\PageVersionMarkedToBeReviewed;
use App\Traits\RetrievesFieldData;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Mtc\ContentManager\Contracts\Content;
use Mtc\ContentManager\Contracts\ContentElement as ContentElementContract;
use Mtc\ContentManager\Contracts\ContentElementField;
use Mtc\ContentManager\Contracts\PageModel;
use Mtc\ContentManager\Contracts\TemplateElement;
use Mtc\ContentManager\Contracts\VersionModel;
use Mtc\ContentManager\Facades\Media;
use Mtc\ContentManager\PageRepository as MainPageRepository;
use Mtc\MercuryDataModels\ContentElement;
use Mtc\MercuryDataModels\Contracts\ModelWithContent;
use Mtc\MercuryDataModels\MediaUse;
use Mtc\MercuryDataModels\Page;
use Mtc\MercuryDataModels\Permission;
use Mtc\MercuryDataModels\Template;
use Mtc\MercuryDataModels\User;

class PageRepository extends MainPageRepository implements InteractsWithContentSync, AbleToSyncContentElements
{
    use SavesSeoData;
    use RetrievesFieldData;

    /**
     * Save Page
     *
     * @param PageModel $page
     * @param array $input
     * @param int $author
     * @return void
     */
    public function save(PageModel $page, array $input, int $author): void
    {
        $has_password = $input['password_protected'] ?? null;
        $page->password = $has_password ? ($input['password'] ?? null) : null;
        $page->franchise_id = $input['franchise_id'] ?? null;
        $page->category = $input['category'] ?? null;
        $page->featured = $input['featured'] ?? null;
        if ($page->template_id !== $input['template_id']) {
            $page->template_id = $input['template_id'];
        }
        parent::save($page, $input, $author);
        Media::setUsesForModel($input['media'] ?? [], $page, ['primary' => true], true);
        $this->saveSeo($page, $input['seo'] ?? []);
        $this->clearCache($page);
    }

    public function copyPage(int $pageId, string $title, bool $withContent)
    {
        /** @var PageModel $original */
        $original = $this->pageModel->newQuery()
            ->with('content.subContent.subContent.subContent.subContent.subContent.subContent')
            ->findOrFail($pageId);

        if (config('pages.use_transactions')) {
            DB::beginTransaction();
        }
        /** @var PageModel $page */
        $page = $this->pageModel->newQuery()
            ->create([
                'status' => 'draft',
                'title' => $title,
                'category' => $original->category,
                'franchise_id' => $original->franchise_id,
                'password' => $original->password,
                'excerpt' => $original->excerpt,
                'featured' => $original->featured,
                'slug' => Str::slug($title),
                'template_id' => $original->template_id
            ]);

        $original->mediaUses->each(fn(MediaUse $mediaUse) => $page->mediaUses()->create($mediaUse->toArray()));
        if ($withContent) {
            $original->content->each(fn(Content $content) => $this->nestedContentCopy($content, $page));
        } elseif ($page->template_id) {
            $this->addContentFromLayout($page);
        }
        if (config('pages.use_transactions')) {
            DB::commit();
        }
        return $page;
    }

    public function isContentOutOfSync(PageModel $page): bool
    {
        return $this->getMissingStructure($page)->isNotEmpty()
            || $this->elementsOnPageOutOfSync($page)->isNotEmpty()
            || $this->getElementsFromDifferentTemplate($page)->isNotEmpty()
            || $this->getOrphanedElements($page)->isNotEmpty();
    }

    protected function nestedContentCopy(Content $source, PageModel $page, ?Content $parent = null): void
    {
        $data = $source->toArray();
        if ($parent) {
            $data['parent_id'] = $parent->id;
        }
        $media = $source->mediaUses()->pluck('media_id');
        /** @var Content $content */
        $content = $page->content()->create($data);
        if ($media->isNotEmpty()) {
            Media::setUsesForModel($media->toArray(), $content);
        }
        $source->subContent->each(fn(Content $subContent) => $this->nestedContentCopy($subContent, $page, $content));
    }

    public function savePageVersion(
        PageModel $page,
        array $content,
        int $author,
        bool $is_active = false
    ): ?VersionModel {
        if ($this->hasVersioning() !== true) {
            return null;
        }

        return parent::savePageVersion($page, $content, $author, $is_active);
    }

    /**
     * @param int $pageId
     * @param int $versionId
     * @return VersionModel|Model
     */
    public function markVersionAsChangesRequested(int $pageId, int $versionId): VersionModel
    {
        /** @var VersionModel $result */
        $result = parent::markVersionAsChangesRequested($pageId, $versionId);
        $result->author->notify(new PageVersionMarkedChangesRequested($result, Auth::user()));

        return $result;
    }

    /**
     * @param int $pageId
     * @param int $versionId
     * @return VersionModel|Model
     */
    public function markVersionForReview(int $pageId, int $versionId): VersionModel
    {
        /** @var VersionModel $result */
        $result = parent::markVersionForReview($pageId, $versionId);
        $publisherRoles = $this->publishContentRoles();
        tenant()
            ->users
            ->filter(fn(User $user) => $publisherRoles->search($user->pivot->role) !== false)
            ->each(fn(User $user) => $user->notify(new PageVersionMarkedToBeReviewed($result)));
        return $result;
    }

    public function canRemove(int $id, ?PageModel $page = null): bool
    {
        if (Auth::user()->hasPermissionTo('publish-content') === false) {
            return false;
        }

        return parent::canRemove($id, $page);
    }

    public function saveVersion(VersionModel $versionModel, array $input, int $author): void
    {
        if ($versionModel->is_active) {
            throw new Exception('Active version cannot be updated!');
        }
        if ($author != $versionModel->author_id) {
            throw new Exception('Version can be updated only by user that is assigned to it!');
        }
        parent::saveVersion($versionModel, $input, $author);
    }

    public function hasVersioning(): bool
    {
        return Settings::get('pages-versioning-enabled') === true
            && TierHelper::isAllowed(tenant('tier'), Tier::STANDARD->value);
    }

    public function indexVersions(Request $request, VersionModel $versionModel)
    {
        $resource = Config::get('pages.version_list_resource');
        return new $resource(
            $versionModel->newQuery()
                ->with([
                    'page.primaryMediaUse.media',
                    'author'
                ])
                ->when(
                    $request->input('filters') === 'pending-review',
                    fn($query) => $query->where('pending_review', 1)
                        ->when(
                            Auth::user()->hasPermissionTo('publish-content'),
                            fn($query) => $query,
                            fn($query) => $query->where('author_id', Auth::id()),
                        ),
                )
                ->when(
                    $request->input('filters') === 'changes-requested',
                    fn($query) => $query->where('request_changes', 1),
                )
                ->paginate()
        );
    }


    protected function getContentElementFieldData(ContentElementField $field)
    {
        $data = array_merge($field->data ?? [], $field->meta ?? []);
        $data['fieldId'] = $field->field_type;

        if (!isset($data['icon'])) {
            $data['icon'] = $this->getFieldIcon($field);
        }
        if (!isset($data['component'])) {
            $data['component'] = 'EditableContent' . $this->getField($field->field_type)?->getComponent();
            $data['componentName'] = $this->getField($field->field_type)?->getComponent();
        }
        return $data;
    }

    private function publishContentRoles(): Collection
    {
        return Permission::query()->where('name', 'publish-content')->firstOrFail()->roles->pluck('name');
    }

    private function elementsOnPageOutOfSync(Page $page): Collection
    {
        return ContentElement::query()
            ->whereIn('id', $page->allContent()->whereNotNull('element_id')->pluck('element_id')->unique())
            ->get()
            ->reject(fn(ContentElement $element) => $this->isContentElementInSync($page, $element));
    }

    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();
    }

    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;
    }

    public function importRecord(array $entry): bool
    {
        if (!empty($entry['template'])) {
            $entry['template_id'] = Template::query()
                ->where('slug', $entry['template'])
                ->first()?->id;
        }

        /** @var PageModel $page */
        $page = $this->pageModel->newQuery()->create($entry);
        if (!empty($entry['content']) && request()->input('details.data')) {
            $this->addContentFromLayout($page);
        }

        if (!empty($entry['media_uses'])) {
            $imagesIds = [];
            foreach ($entry['media_uses'] as $image) {
                $imagesIds[] = Media::importImageFromUrl($image)->id;
            }
            Media::setUsesForModel($imagesIds, $page);
        }

        return $page->exists;
    }

    public function canBeImported(array $entry): bool
    {
        if (request()->input('details.data')) {
            return collect($entry['content'])
                ->filter(fn($element) => $this->unknownContentElement($element))
                ->isEmpty();
        }

        return true;
    }

    public function exportToRemote(array $selections): array
    {
        return $this->pageModel->newQuery()
            ->with([
                'mediaUses.media',
                'content',
            ])
            ->whereIn('id', $selections)
            ->get()
            ->map(function (PageModel $page) {
                $data = $page->toArray();

                $data['media_uses'] = $page->mediaUses
                    ->map(function ($mediaUse) {
                        return $mediaUse->media?->getOriginalUrlAttribute();
                    })
                    ->filter()
                    ->toArray();

                unset($data['template_id']);
                $data['template'] = $page->template->slug;

                $data['content'] = $page->allContent
                    ->map(function ($field) {
                        return $field?->slug;
                    });

                return $data;
            })
            ->toArray();
    }

    public function checkImportEntryValidity(array $dataEntry, array $allEntries): array
    {
        $errors = [];
        if (empty($dataEntry['slug'])) {
            $errors[] = __('validation.import_slug_missing', ['slug' => $dataEntry['slug']]);
        } elseif ($this->pageModel->newQuery()->where('slug', $dataEntry['slug'])->exists()) {
            $errors[] = __('validation.import_slug_taken', ['slug' => $dataEntry['slug']]);
        }

        if (!$this->canBeImported($dataEntry)) {
            $errors[] = "Missing content elements! Sync them first!";
        }

        return [
            'data' => $dataEntry,
            'errors' => $errors,
        ];
    }

    private function unknownContentElement($slug): bool
    {
        return ContentElement::query()
            ->where('slug', $slug)
            ->exists() === false;
    }

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

        /** @var Page $temp_page */
        $temp_page = Page::query()->find($entry_id);
        $this->elementsOnPageOutOfSync($temp_page)
            ->each(fn(ContentElement $element) => $this->syncSpecificElementOnPage($entry_id, $element));

        $model = $this->loadModelWithSyncRelationships($entry_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($model, $this->getElementsFromDifferentTemplate($model));
        $this->dropTemplateContent($model, $this->getOrphanedElements($model));
    }

    protected function syncSpecificElementOnPage(int $page_id, ContentElement $element): void
    {
        /** @var ?Page $page */
        $page = Page::query()
            ->with('allContent.subContent')
            ->find($page_id);
        if (!$page) {
            return;
        }

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

    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 dropRemovedFieldEntriesFromContent(Model $parent_content, Collection $field_slugs): void
    {
        $parent_content->subContent
            ->whereNotIn('slug', $field_slugs)
            ->each(fn(Content $contentEntry) => $contentEntry->delete());
    }

    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): Page
    {
        return Page::query()
            ->with([
                'content.subContent.subContent.subContent.subContent',
                'template.elements.element.fields.childElement.fields.childElement'
                . '.fields.childElement.fields.childElement',
            ])
            ->findOrFail($model_id);
    }

    private function clearCache(PageModel $page): void
    {
        $access_param =  '?' . http_build_query([
            'a' => base64_encode($page->id . '-' . $page->slug),
        ]);
        Cache::forget(tenant('id') . '-page-view-/frontend/pages/' . $page->slug);
        Cache::forget(tenant('id') . '-page-view-/frontend/pages/' . $page->slug . $access_param);
    }
}
