<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;

use App\Models\Page;
use App\Models\Blog;
use App\Models\Announcement;
use App\Models\Course;
use App\Models\Version;
use App\Models\Contentable;
use App\Models\TextBlock;
use App\Models\PhotoBlock;
use App\Models\YoutubeVideo;
use App\Models\Quote;
use App\Models\Slideshow;
use App\Models\ContentFilter;
use App\Models\CourseDescription;
use App\Models\PublicationList;
use App\Models\Calendar;
use App\Models\CourseList;
use App\Models\LivestreamList;
use App\Models\StaffProfileList;
use App\Models\EmbedVideo;
use App\Models\SocialMediaFeed;

use App\Events\ContentElementSaved;
use App\Events\ContentElementCreated;

use App\Traits\TagsTrait;
use App\Traits\HasPermissionsTrait;

use App\Contracts\SearchResultContract;
use App\Utilities\SearchResult;
use App\Utilities\PageLink;

class ContentElement extends Model implements SearchResultContract
{
    use HasFactory;
    use SoftDeletes;
    use TagsTrait;
    use HasPermissionsTrait;

    protected $with = ['permissions.accessable'];
    //protected $with = ['content', 'contentables', 'contentables.version', 'tags'];
    //protected $appends = ['type', 'actions', 'has_permissions'];

    protected $appends = ['type'];

    protected $casts = [
        'publish_at' => 'datetime',
    ];

    public function loadWith()
    {
        return ['contentables.version', 'tags', 'content.photos.fileUpload'];
    }

    public function appendWith()
    {
        return ['actions', 'has_permissions', 'anchor'];
    }

    public function saveContentElement(array $input, $id = null)
    {
        $contentable = self::findContentable($input);

        $old_contentable = null;
        if (Arr::get($input, 'pivot.old_contentable_id') && Arr::get($input, 'pivot.old_contentable_type')) {
            $old_contentable = self::findContentable([
                'pivot' => [
                    'contentable_id' => Arr::get($input, 'pivot.old_contentable_id'),
                    'contentable_type' => Arr::get($input, 'pivot.old_contentable_type'),
                ]
            ]);
        }

        $uuid = null;
        $new_version = true;
        $permissions = collect();
        $old_contentables = null;

        if ($id) {
            $content_element = ContentElement::findOrFail($id);
            $uuid = $content_element->uuid;

            $old_contentables = $content_element->contentables()->get();

            $is_published = $content_element->contentables()->whereHas('version', function ($query) {
                $query->whereNotNull('published_at');
            })->count();

            if (!$is_published) {
                $new_version = false;
            } else {
                $content_elements = ContentElement::where('uuid', $uuid)
                    ->whereHas('contentables', function ($query) use ($contentable) {
                        $query->where('contentable_id', $contentable->id)
                              ->where('contentable_type', get_class($contentable))
                                ->whereHas('version', function ($query) {
                                    $query->whereNull('published_at');
                                });
                    })
                    ->with('contentables')
                    ->get()
                    ->sortByDesc(function ($content_element) use ($contentable) {
                        return $content_element->getPageVersion($contentable)->id;
                    });

                if ($content_elements->count()) {
                    $content_element = $content_elements->first();
                } else {
                    $permissions = $content_element->permissions;
                    $content_element = new ContentElement();
                    $content_element->uuid = $uuid;
                }
            }
        } else {
            $content_element = new ContentElement();
            $content_element->uuid = Str::uuid();
        }

        $content_class = 'App\\Models\\'.Str::studly(Arr::get($input, 'type'));
        $content = (new $content_class())->saveContent(Arr::get($input, 'content'), $new_version ? null : Arr::get($input, 'content.id'));

        $content_element->content_id = $content->id;
        $content_element->content_type = get_class($content);

        //$content_element->version_id = $contentable->getDraftVersion()->id;

        $content_element->publish_at = Arr::get($input, 'publish_at');

        if (auth()->check() && Arr::get($input, 'publish_at')) {
            $content_element->publisher_id = auth()->user()->id;
        }

        $content_element->save();

        $content_element->saveTags($input);

        // assign or update the content element to the contentable

        if ($uuid) {
            $pages = self::findPagesByUuid($uuid, $contentable);
        } else {
            $pages = collect([$contentable]);
        }

        foreach ($pages as $page) {
            $pivot_data = $content_element->contentables()
                                           ->where('contentable_id', $page->id)
                                           ->where('contentable_type', get_class($page))
                                           ->first();

            if (!$pivot_data && $new_version && $old_contentables) {
                $pivot_data = $old_contentables->first(function ($old) use ($page) {
                    return $old->contentable_id === $page->id && $old->contentable_type === get_class($page);
                });
            }

            $pivot = array();
            $pivot['version_id'] = $page->getDraftVersion()->id;

            if ($page->id === $contentable->id && get_class($page) === get_class($contentable) && request('instance')) {
                $pivot['deleted_at'] = null;
            }

            if ($pivot_data) {
                $pivot['sort_order'] = $pivot_data->sort_order;
                $pivot['expandable'] = $pivot_data->expandable;
                $pivot['unlisted'] = $pivot_data->unlisted;
                $pivot['guest'] = $pivot_data->guest;
                $pivot['no_margin'] = $pivot_data->no_margin;
                $pivot['randomize'] = $pivot_data->randomize;
                $pivot['filter'] = $pivot_data->filter;
                $pivot['hide_print'] = $pivot_data->hide_print;
            }

            if ($page->id === $contentable->id && get_class($page) === get_class($contentable)) {
                $pivot['sort_order'] =  Arr::get($input, 'pivot.sort_order');
                $pivot['expandable'] =  Arr::get($input, 'pivot.expandable');
                $pivot['unlisted'] = Arr::get($input, 'pivot.unlisted');
                $pivot['guest'] = Arr::get($input, 'pivot.guest');
                $pivot['no_margin'] = Arr::get($input, 'pivot.no_margin');
                $pivot['randomize'] = Arr::get($input, 'pivot.randomize');
                $pivot['filter'] = Arr::get($input, 'pivot.filter');
                $pivot['hide_print'] = Arr::get($input, 'pivot.hide_print');
            }

            if ($old_contentable) {
                // we have moved this to some other page so mark as deleted
                if ($page->id === $old_contentable->id && get_class($page) === get_class($old_contentable)) {
                    $pivot['deleted_at'] = now();
                }
            }

            if (!$page->contentElements()->get()->contains('id', $content_element->id)) {
                $page->contentElements()->attach($content_element, $pivot);
            } else {
                $page->contentElements()->updateExistingPivot($content_element->id, $pivot);
            }

            cache()->tags([cache_name($contentable)])->flush();
        }

        // add any permissions for a new version of the content element
        if ($permissions->count()) {
            foreach ($permissions as $permission) {
                $content_element->createPermission($permission->action, $permission->accessable);
            }
        }

        // refresh the content element so that it updates its content
        $content_element->refresh();
        cache()->tags([cache_name($content_element)])->flush();

        if ($new_version) {
            broadcast(new ContentElementCreated($content_element, $contentable))->toOthers();
        } else {
            broadcast(new ContentElementSaved($content_element, $contentable))->toOthers();
        }

        return $content_element;
    }

    public static function findPagesByUuid($uuid, $add_page = null)
    {
        $pages = ContentElement::where('uuid', $uuid)
             ->with('contentables') // we can't eager load the nested pageable relationship
             ->get()
             ->map->contentables
             ->flatten()
             ->filter(function ($contentable) {
                 return $contentable->pageable ? true : false;
             })
             ->groupBy(function ($contentable) {
                 return $contentable->pageable->type.'-'.$contentable->pageable->id;
             })
             ->map(function ($group) {
                 $latest = $group->sortByDesc(function ($contentable) {
                     return $contentable->version_id;
                 })->first();

                 if ($latest->deleted_at) {
                     return null;
                 }
                 return $latest->pageable;
             })
             ->flatten()
             ->filter();

        if ($add_page) {
            $pages->push($add_page);
        }

        return $pages->unique(function ($item) {
            return $item->type.$item->id;
        });
    }

    // TODO replace this with PolyFinder::pageable
    public static function findContentable($input)
    {
        if (Str::contains(Arr::get($input, 'pivot.contentable_type'), 'App\\Models\\')) {
            $class_name = Arr::get($input, 'pivot.contentable_type');
        } else {
            $class_name = 'App\\Models\\'.Str::studly(Arr::get($input, 'pivot.contentable_type'));
        }

        Validator::make($input, [
            'pivot.contentable_id' => ['required', function ($attribute, $value, $fail) use ($input, $class_name) {
                $id_check = resolve($class_name)->find($value);
                if (!$id_check) {
                    $fail('No related object found when saving the content element');
                }
            }],
            'pivot.contentable_type' => ['required', function ($attribute, $value, $fail) use ($input, $class_name) {
                $class = resolve($class_name);
                if (!$class) {
                    $fail('No related class found when saving the content element');
                }
            }],
        ])->validate();

        return (new $class_name())->findOrFail(Arr::get($input, 'pivot.contentable_id'));
    }

    public function contentables()
    {
        return $this->hasMany(Contentable::class);
    }

    public function pages()
    {
        return $this->morphedByMany(Page::class, 'contentable')->withPivot(
            'sort_order',
            'unlisted',
            'expandable',
            'version_id',
            'deleted_at',
            'guest',
            'no_margin',
            'randomize',
            'filter',
            'hide_print',
        );
    }

    public function blogs()
    {
        return $this->morphedByMany(Blog::class, 'contentable')->withPivot(
            'sort_order',
            'unlisted',
            'expandable',
            'version_id',
            'deleted_at',
            'guest',
            'no_margin',
            'randomize',
            'filter',
            'hide_print',
        );
    }

    public function announcements()
    {
        return $this->morphedByMany(Announcement::class, 'contentable')->withPivot(
            'sort_order',
            'unlisted',
            'expandable',
            'version_id',
            'deleted_at',
            'guest',
            'no_margin',
            'randomize',
            'filter',
            'hide_print',
        );
    }

    public function courses()
    {
        return $this->morphedByMany(Course::class, 'contentable')->withPivot(
            'sort_order',
            'unlisted',
            'expandable',
            'version_id',
            'deleted_at',
            'guest',
            'no_margin',
            'randomize',
            'filter',
            'hide_print',
        );
    }

    public function content()
    {
        return $this->morphTo();
    }

    public function publisher()
    {
        return $this->belongsTo(User::class, 'publisher_id');
    }

    public function versions()
    {
        return $this->belongsToMany(Version::class, 'contentables');
    }

    public function getTypeAttribute()
    {
        return cache()->tags([cache_name($this)])->rememberForever(cache_name($this).'-type', function () {
            return Str::kebab(class_basename($this->content));
        });
    }

    public function getAnchorAttribute()
    {
        return PageLink::convertAnchorText($this->content->anchor_field ?? $this->uuid);
    }

    public function getPublishedVersion()
    {
        return (new ContentElement())->where('uuid', $this->uuid)
            ->with('versions')
            ->get()
            ->sortByDesc(function ($content_element) {
                $published_versions = $content_element->versions->filter(function ($version) {
                    return $version->published_at ? true : false;
                })
                ->sortByDesc(function ($version) {
                    return $version->published_at;
                })
                ->values();

                if ($published_versions->count()) {
                    return $published_versions->first()->published_at;
                }
                return null;
            })
            ->filter()
            ->values()
            ->first();
    }

    public function getPreviousVersion($page, $version = null)
    {
        if (!$version) {
            $version = $page->getDraftVersion();
        }

        return ContentElement::where('uuid', $this->uuid)
            ->whereHas('contentables', function ($query) use ($page, $version) {
                $query->where('contentable_id', $page->id)
                    ->where('contentable_type', get_class($page))
                    ->where('version_id', '<', $version->id);
            })
            ->get()
            ->sortByDesc(function ($content_element) use ($page) {
                return $content_element->contentables()
                    ->where('contentable_id', $page->id)
                    ->where('contentable_type', get_class($page))
                    ->first()
                    ->version->id;
            })->first();
    }

    public function isType($type)
    {
        return $this->type === $type;
    }

    public function getPageVersion($contentable)
    {
        $contentables = $this->contentables
                ->where('contentable_id', $contentable->id)
                ->where('contentable_type', get_class($contentable))
                ->load('version');

        if ($contentables->count() === 1) {
            return $contentables->first()->version;
        } elseif (!$contentables->count()) {
            throw new ModelNotFoundException('No version found');
        } elseif ($contentables->count() > 1) {
            throw new ModelNotFoundException('More than one contentable found');
        }
    }

    public function getSearchFieldsAttribute()
    {
        return [];
    }

    public static function searchResults($terms, Collection $filters)
    {
        $content_elements = self::where(function ($query) use ($terms) {
            $query->whereHasMorph(
                'content',
                [
                    TextBlock::class,
                    PhotoBlock::class,
                    YoutubeVideo::class,
                    Quote::class,
                    Slideshow::class,
                    ContentFilter::class,
                    CourseDescription::class,
                    PublicationList::class,
                    Calendar::class,
                    CourseList::class,
                    LivestreamList::class,
                    StaffProfileList::class,
                    EmbedVideo::class,
                    SocialMediaFeed::class,
                ],
                function (Builder $query, $type) use ($terms) {
                    $first = true;
                    foreach ($terms as $term) {
                        $fields = (new $type())->search_fields;
                        if ($fields) {
                            foreach ($fields as $field) {
                                if ($first) {
                                    $query->where($field, 'LIKE', '%'.$term.'%');
                                    $first = false;
                                } else {
                                    $query->orWhere($field, 'LIKE', '%'.$term.'%');
                                }
                            }
                        }
                    }
                }
            );
            $query->orWhereHas('tags', function ($query) use ($terms) {
                $first = true;
                foreach ($terms as $term) {
                    if ($first) {
                        $query->where('name', 'LIKE', '%'.$term.'%');
                        $first = false;
                    } else {
                        $query->orWhere('name', 'LIKE', '%'.$term.'%');
                    }
                }
            });
        })
        ->where(function ($query) use($filters) {
            //if (!auth()->check()) {
                $query->whereHas('contentables', function ($query) use($filters) {
                    $query->where('unlisted', 0)
                        ->whereHas('version', function ($query) {
                        $query->whereNotNull('published_at')
                            ->where('search_exclude', 0);
                        });

                        if ($filters->count()) {
                            $filters = $filters->map(function($filter) {
                                return 'App\\Models\\'.Str::title(Str::singular($filter));
                            })->toArray();
                            $query->whereIn('contentable_type', $filters);
                        }
                });
            //}
        })
        ->with('contentables.version.versionable.version', 'contentables.contentElement.content.photos')
        ->get();

        $content_elements = $content_elements->groupBy('uuid')
            ->map(function ($uuid) {
                return $uuid->sortByDesc('id')->first();
            })
            ->flatten()
            ->map(function ($content_element) use ($filters) {
                return $content_element->contentables->filter(function ($contentable) use ($filters) {
                    $type = Str::plural(Str::lower(class_basename($contentable->version->versionable)));

                    if ($filters->count()) {
                        if (!$filters->contains($type)) {
                            return false;
                        }
                    }

                    if (auth()->check()) {
                        if (!$contentable->version?->published_at) {
                            if (!auth()->user()->can('update', $contentable->version?->versionable)) {
                                return false;
                            }
                        } else {
                            if ($contentable->unlisted) {
                                return false;
                            }
                        }

                        if ($contentable->version?->versionable?->hasViewPermissions()) {
                            return auth()->user()->can('view', $contentable->version?->versionable);
                        }
                    } else {
                        if ($contentable->unlisted) {
                            return false;
                        }

                        if ($contentable->version?->versionable?->hasViewPermissions()) {
                            return false;
                        }
                    }
                    return $contentable->version?->versionable?->full_slug ? true : false;
                });
            })
            ->flatten()
            ->groupBy(function ($contentable) {
                return $contentable->contentable_id.'-'.Str::snake(class_basename($contentable->contentable_type)).'-'.$contentable->contentElement->uuid;
            })
            ->map(function ($group) use ($terms) {
                $contentable = $group->sortByDesc(function ($contentable) {
                    return $contentable->version->published_at;
                })->first();

                $content_element = $contentable->contentElement;
                $page = $contentable->version->versionable;

                $rank = $content_element->getSearchResultRank($terms);

                if ($page->type === 'page') {
                    $rank = $rank * 1.5;
                }

                $preview = $content_element->getSearchResultPreview($terms);

                if (mb_strlen($preview) < 1) {
                    $preview = $page->getSearchResultPreview($terms);
                }

                if ($page->version->search_exclude) {
                    return null;
                }

                return new SearchResult(
                    $page->type,
                    $page->id,
                    '/'.$page->full_slug.'#'.PageLink::convertAnchorText($content_element->content->header),
                    $content_element->getSearchResultTitle($page->getSearchResultTitle()),
                    $preview,
                    $rank,
                    SearchResult::urlText($page->full_slug.'/'.$content_element->content->header),
                    $contentable->version->published_at ?? $content_element->updated_at,
                    $content_element->getSearchResultPhoto($terms),
                );
            })
            ->filter()
            ->flatten();

        return $content_elements;
    }

    public function getSearchResultTitle($default = null)
    {
        return $this->content->getSearchResultTitle($default);
    }

    public function getSearchResultPreview($terms)
    {
        return $this->content->getSearchResultPreview($terms);
    }

    public function getSearchResultRank($terms)
    {
        return $this->content->getSearchResultRank($terms);
    }

    public function getSearchResultPhoto($terms)
    {
        $photos = $this->content->photos;

        if ($photos?->count()) {
            $photo = $photos->first(function ($photo) use ($terms) {
                if ($photo->name) {
                    if (Str::contains($photo->name, $terms)) {
                        return true;
                    }
                }

                if ($photo->description) {
                    if (Str::contains($photo->description, $terms)) {
                        return true;
                    }
                }

                if ($photo->alt) {
                    if (Str::contains($photo->alt, $terms)) {
                        return true;
                    }
                }

                return false;
            });
        } else {
            $photo = null;
        }

        return $photo ?? $photos?->first();
    }

    public static function filterByPermissions(Collection $collection, $permission = 'view')
    {
        return $collection->filter(function ($content_element) use ($permission) {
            // if guest only content then filter if authed
            if ($content_element->pivot->guest && auth()->check() && !editing()) {
                return false;
            }

            // if there are no permissions anyone can view it
            if (!$content_element->permissions->where('action', $permission)->count()) {
                return true;
            }
            // if there are permissions and there is no one logged in hide it
            if (!auth()->check()) {
                return false;
            }

            // check if the current use can view the content element
            return auth()->user()->can($permission, $content_element);
        });
    }

    public function createNewVersionOnPage($page)
    {
        $contentable = $this->contentables()
                        ->where('contentable_id', $page->id)
                        ->where('contentable_type', get_class($page))
                        ->first();

        $input = $this->load($this->loadWith())->toArray();
        $input['pivot']['contentable_id'] = $page->id;
        $input['pivot']['contentable_type'] = get_class($page);
        $input['pivot']['sort_order'] = $contentable->sort_order;
        $input['pivot']['unlisted'] = $contentable->unlisted;
        $input['pivot']['expandable'] = $contentable->expandable;
        $input['pivot']['guest'] = $contentable->guest;
        $input['pivot']['no_margin'] = $contentable->no_margin;
        $input['pivot']['randomize'] = $contentable->randomize;
        $input['pivot']['filter'] = $contentable->filter;
        $input['pivot']['hide_print'] = $contentable->hide_print;

        return (new ContentElement())->saveContentElement($input, $this->id);
    }

    public function findNextIdByType($content_elements, $page)
    {
        return cache()->tags([cache_name($this), cache_name($page)])->rememberForever(cache_name($this).'-next-id', function() use($content_elements) {
            $same_content_type = $content_elements->filter->isType($this->type)->map(function ($ce, $index) {
                return [ $index => $ce->id ];
            })->values();

            $current_index = $same_content_type->search(function ($item) {
                return collect($item)->first() === $this->id;
            });
            if ($same_content_type->count() > $current_index + 1) {
                $next_id = collect($same_content_type[$current_index + 1])->first();
            } else {
                $next_id = null;
            }
            return $next_id;
        });
    }

    public function renderView($page, $content, $dash, $header, $sort_order)
    {
        return cache()->tags([cache_name($this)])->rememberForever(cache_name($this).'-rendered-v'.$page->version?->id.'-'.(editing() ? 'editing' : 'published'), function () use ($page, $content, $dash, $header, $sort_order) {
            return view('content-elements.'.$this->type, [
                'content_element' => $this,
                'page' => $page,
                'content' => $this->content,
                'first' => $content->filter->isType($this->type)->first()?->id === $this->id,
                'next_id', $this->findNextIdByType($content, $page),
                'dash' => $dash,
                'header' => $header,
                'sort_order' => $sort_order,
            ])->render();
        });
    }
}
