<?php

namespace Common\Channels;

use App\Models\Channel;
use BadMethodCallException;
use Common\Core\Prerender\Actions\ReplacePlaceholders;
use Common\Database\Datasource\Datasource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class LoadChannelContent
{
    public function execute(
        Channel $channel,
        array $params = [],
        Channel $parent = null,
    ): ?AbstractPaginator {
        $params['perPage'] = $params['perPage'] ?? 50;
        $params['paginate'] = $this->resolvePaginateType($channel, $params);
        if (!isset($params['orderBy']) && !isset($params['order'])) {
            $params['order'] = Arr::get($channel->config, 'contentOrder');
        }

        if (
            $channel->shouldRestrictContent()
            // Arr::get($params, 'loader') !== 'editChannelPage'
        ) {
            $this->applyRestriction($channel, $params, $parent);
            // If restriction could not be loaded bail. This is used to cancel content loading and return 404,
            // if, for example, loading genre channel, but specified genre does not exist.
            if (!$channel->restriction) {
                return new Paginator([], 50);
            }
        }

        $contentType = Arr::get($channel->config, 'contentType');

        if ($contentType === 'listAll') {
            return $this->paginateAllContentFromDatabase(
                $channel,
                $params,
                $parent,
            );
        } else {
            // only cache channels that have other nested channels as content
            $shouldCache =
                Arr::get($channel->config, 'contentModel') ===
                Channel::MODEL_TYPE;
            if (!$shouldCache) {
                return $this->loadCuratedContent($channel, $params);
            } else {
                $paramsKey = json_encode($params);
                return Cache::remember(
                    // use "updated at" so channel changes from admin area will automatically
                    // cause new cache item, without having to clear cache manually
                    "channels.$channel->id.$channel->updated_at.$paramsKey",
                    now()->addHours(24),
                    fn() => $this->loadCuratedContent($channel, $params),
                );
            }
        }
    }

    private function paginateAllContentFromDatabase(
        Channel $channel,
        array $params,
        Channel $parent = null,
    ): AbstractPaginator {
        $contentModel = Arr::get($channel->config, 'contentModel');
        $methodName = sprintf('all%s', ucfirst(Str::plural($contentModel)));
        // if channel specifies a method to load this model, use that
        if (method_exists($channel, $methodName)) {
            return $channel->{$methodName}($params, null, $parent);
            // otherwise do a basic pagination for the model
        } else {
            $namespace = modelTypeToNamespace($contentModel);
            $datasource = new Datasource(app($namespace)::query(), $params);
            return $datasource->paginate();
        }
    }

    private function loadCuratedContent(
        Channel $channel,
        array $params,
    ): AbstractPaginator {
        $contentModel = Arr::get($channel, 'config.contentModel');
        $methodName = sprintf('all%s', ucfirst(Str::plural($contentModel)));
        $builder = $channel->{Str::plural($contentModel)}();

        // if channel specifies a method to load this model, use that
        if (method_exists($channel, $methodName)) {
            $pagination = $channel->{$methodName}($params, $builder);
        } else {
            $datasource = new Datasource($builder, $params);
            $order = $datasource->getOrder();

            // get only column name, in case it's prefixed with table name
            if (last(explode('.', $order['col'])) === 'popularity') {
                $datasource->order = false;
                try {
                    $builder->orderByPopularity($order['dir']);
                } catch (BadMethodCallException $e) {
                    //
                }
            }
            $pagination = $datasource->paginate();
        }

        $pagination
            ->filter(fn($model) => $model->pivot)
            ->transform(function (Model $model) use ($channel, $params) {
                $model['channelable_id'] = $model->pivot->id;
                $model['channelable_order'] = $model->pivot->order;
                if ($model instanceof Channel) {
                    $model->loadContent(
                        array_merge($params, [
                            // clear parent channel pagination params and only load 12 items per nested channel
                            'perPage' => 12,
                            'page' => 1,
                            'paginate' => 'simple',
                            // clear this so nested channel always uses sorting order set in that channel's config
                            'order' => null,
                        ]),
                        $channel,
                    );
                }
                return $model;
            });
        return $pagination;
    }

    private function applyRestriction(
        Channel $channel,
        array $params = [],
        Channel $parent = null,
    ): void {
        $urlParam = Arr::get($params, 'restriction');
        $restriction =
            $parent->restriction ?? $channel->loadRestrictionModel($urlParam);
        if ($restriction) {
            $channel->setAttribute('restriction', $restriction);
            $channel->name =
                app(ReplacePlaceholders::class)->execute($channel->name, [
                    'channel' => $channel,
                ]) ?:
                $channel->name;
        }
    }

    protected function resolvePaginateType(
        Channel $channel,
        array $params,
    ): string {
        if (isset($params['paginate'])) {
            return $params['paginate'];
        }

        if (isset($channel->config['paginationType'])) {
            return match ($channel->config['paginationType']) {
                'lengthAware' => 'lengthAware',
                // simple and infinite scroll
                default => 'simple',
            };
        }

        return 'simple';
    }
}
