<?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\ModelWithContent;
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\MediaUse;
use Mtc\MercuryDataModels\Page;
use Mtc\MercuryDataModels\PageVersion;
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->elementsOnModelOutOfSync($page)->isNotEmpty()
            || $this->getElementsFromDifferentTemplate($page, $page->template_id)->isNotEmpty()
            || $this->getOrphanedElements($page, $page->template_id)->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 (!empty($versionModel->author_id) && $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');
    }

    protected function elementsOnModelOutOfSync(ModelWithContent $model): Collection
    {
        return ContentElement::query()
            ->whereIn('id', $model->allContent()->whereNotNull('element_id')->pluck('element_id')->unique())
            ->get()
            ->reject(fn(ContentElement $element) => $this->isContentElementInSync($model, $element));
    }


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


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

    protected function syncSpecificElementOnPage(int $page_id, ContentElementContract $element): void
    {
        /** @var ?Page $page */
        $page = $this->pageModel->newQuery()
            ->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 dropRemovedFieldEntriesFromContent(Model $parent_content, Collection $field_slugs): void
    {
        $parent_content->subContent
            ->whereNotIn('slug', $field_slugs)
            ->each(fn(Content $contentEntry) => $contentEntry->delete());
    }

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

    protected function canRequestRevisionChanges(VersionModel $version): bool
    {
        return Settings::get('pages-versioning-content-review-mode') ?? false;
    }

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