<?php

namespace Ignite\Core\Entities;

use ArrayAccess;
use Ignite\Core\Facades\Program;
use Ignite\Core\Models\Purifier;
use Ignite\Core\Traits\AttributeToggle;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;

/**
 * Page Entity
 *
 * @property int $id
 * @property string $label
 * @property string|null $code
 * @property string|null $locale
 * @property string $content
 * @property string|null $meta_title
 * @property string|null $meta_description
 * @property string|null $meta_keywords
 * @property bool $is_home
 * @property bool $is_alias
 * @property bool $display_in_menu
 * @property bool $is_protected
 * @property string|null $allowed_groups
 * @property string|null $program_type
 * @property string $template
 * @property int|null $position
 * @property string $status
 * @property Carbon|null $created_at
 * @property Carbon|null $updated_at
 * @property Carbon|null $published_at
 * @property-read \Illuminate\Database\Eloquent\Collection|\Ignite\Core\Entities\Audit[] $audits
 * @property-read int|null $audits_count
 * @property-read string $published_ago
 * @method static Builder|Page byCode($code)
 * @method static Builder|Page isHome()
 * @method static Builder|Page newModelQuery()
 * @method static Builder|Page newQuery()
 * @method static Builder|Page onlyActive()
 * @method static Builder|Page onlyPublished()
 * @method static Builder|Page onlyVisibleInMenu()
 * @method static Builder|Page query()
 * @method static Builder|Page whereAllowedGroups($value)
 * @method static Builder|Page whereCode($value)
 * @method static Builder|Page whereContent($value)
 * @method static Builder|Page whereCreatedAt($value)
 * @method static Builder|Page whereDisplayInMenu($value)
 * @method static Builder|Base whereHasPermission(string $permission, ?\Ignite\Core\Entities\User $user = null)
 * @method static Builder|Page whereId($value)
 * @method static Builder|Page whereIsAlias($value)
 * @method static Builder|Page whereIsHome($value)
 * @method static Builder|Page whereIsProtected($value)
 * @method static Builder|Page whereLabel($value)
 * @method static Builder|Page whereLocale($value)
 * @method static Builder|Page whereMetaDescription($value)
 * @method static Builder|Page whereMetaKeywords($value)
 * @method static Builder|Page whereMetaTitle($value)
 * @method static Builder|Page wherePosition($value)
 * @method static Builder|Page whereProgramType($value)
 * @method static Builder|Page wherePublishedAt($value)
 * @method static Builder|Page whereStatus($value)
 * @method static Builder|Page whereTemplate($value)
 * @method static Builder|Page whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Page extends Base implements AuditableContract
{
    use AttributeToggle;
    use Auditable;

    public const ACTIVE = 'active';
    public const INACTIVE = 'inactive';

    public const MENU_VISIBLE = 1;
    public const MENU_INVISIBLE = 0;

    public const PROGRAM_ANY = 'any';
    public const PROGRAM_CASH = 'cash';
    public const PROGRAM_MERCHANDISE = 'merchandise';

    /**
     * The database table name.
     * @var string
     */
    protected $table = 'core_page';

    /**
     * An array of attribute names which can be toggled.
     * @var array
     */
    protected $toggleable = [
        'status', 'display_in_menu',
    ];

    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = [
        'published_ago',
    ];

    /**
     * Cast values coming from the database into PHP types.
     * @var array
     */
    protected $casts = [
        'is_home' => 'boolean',
        'is_alias' => 'boolean',
        'is_protected' => 'boolean',
        'display_in_menu' => 'boolean',
        'published_at' => 'datetime',
    ];

    /**
     * Allow programmatic overriding of the navigation menu canBeViewed check.
     * @var null|bool
     */
    protected $allowCanBeViewed;

    /**
     * Custom Audit Driver
     *
     * @var \Ignite\Core\Audit\PageDriver
     */
    protected $auditDriver = \Ignite\Core\Audit\PageDriver::class;

    /**
     * Attributes to exclude from the Audit.
     *
     * @var array
     */
    protected $auditExclude = [
        'id',
        'updated_at',
        'created_at',
    ];

    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot()
    {
        parent::boot();

        // When saving the homepage, exclude the code
        static::saving(function (Page $page) {
            if ($page->isHome()) {
                $page->attributes['code'] = "";
            }

            // Purify
            $purifier = app(Purifier::class);
            $page->attributes['content'] = $purifier->clean($page->attributes['content']);
        });

        // Add a default order scope
        static::addGlobalScope('order', function (Builder $builder) {
            $builder->orderBy(DB::raw('CASE WHEN position is null THEN 1 ELSE 0 END'))
                ->orderBy('position', 'asc');
        });
    }

    /**
     * The url of the edit page.
     *
     * @return string
     */
    public function getEditUrl()
    {
        return url('/admin/cms/edit', ['id' => $this->getKey()]);
    }

    /**
     * The published at date in human readable form.
     *
     * @return string
     */
    public function getPublishedAgoAttribute()
    {
        return $this->attributes['published_ago'] = $this->published_at->diffForHumans();
    }

    // ----- Mutators

    /**
     * Set the allowed groups.
     *
     * @param mixed $value
     */
    public function setAllowedGroupsAttribute($value)
    {
        if (is_array($value) || $value instanceof ArrayAccess) {
            if (! in_array('1', $value)) {
                $value[] = '1';
            }
            $value = implode(';', $value);
        }

        // Always set the IT group as allowed.
        if (empty($value)) {
            $value = '1';
        }

        // Make sure there are no spaces.
        $value = trim($value);
        $value = preg_split('/[,;|\s]+/', trim($value));
        sort($value);
        $value = implode(';', $value);

        $this->attributes['allowed_groups'] = $value;
    }

    // ----- Scopes

    /**
     * Query for a URL code.
     *
     * @param Builder $query
     * @param string $code
     * @return Builder
     */
    public function scopeByCode(Builder $query, $code)
    {
        $query->where('code', $code);

        return $query;
    }

    /**
     * Query to find the page marked as home.
     *
     * @param Builder $query
     * @return Builder
     */
    public function scopeIsHome(Builder $query)
    {
        $query->where('is_home', 1);

        return $query;
    }

    /**
     * Query for an active page only.
     *
     * @param Builder $query
     * @return Builder
     */
    public function scopeOnlyActive(Builder $query)
    {
        $query->where('status', Page::ACTIVE);

        return $query;
    }

    /**
     * Query for a page visible in the menu.
     *
     * @param Builder $query
     * @return Builder
     */
    public function scopeOnlyVisibleInMenu(Builder $query)
    {
        $query->where('display_in_menu', Page::MENU_VISIBLE);

        return $query;
    }

    /**
     * Query for a URL code.
     *
     * @param Builder $query
     * @return Builder
     */
    public function scopeOnlyPublished(Builder $query)
    {
        $query->where('published_at', '<=', \Carbon\Carbon::now());

        return $query;
    }

    // -----

    /**
     * Switch status.
     *
     * @param mixed $value
     * @return string
     */
    public function toggleStatus($value)
    {
        return strtolower($value) === static::ACTIVE
            ? static::INACTIVE
            : static::ACTIVE;
    }

    /**
     * Switch display in menu.
     *
     * @param mixed $value
     * @return string
     */
    public function toggleDisplayInMenu($value)
    {
        return (int)$value === static::MENU_VISIBLE
            ? static::MENU_INVISIBLE
            : static::MENU_VISIBLE;
    }

    // -----

    /**
     * The url of the view page.
     *
     * @return string
     */
    public function getCode()
    {
        return $this->code;
    }

    /**
     * The url of the view page.
     *
     * @return string
     */
    public function getUrl()
    {
        return url('/'.$this->getCode());
    }

    /**
     * The page name label.
     *
     * @return string
     */
    public function getLabel()
    {
        return $this->label;
    }

    /**
     * Get the HTML content.
     *
     * @return string
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * Determine if there is any content.
     *
     * @return bool
     */
    public function hasContent()
    {
        return ! empty(strip_tags($this->content ?? ''));
    }

    /**
     * Is the page the homepage.
     *
     * @return bool
     */
    public function isHome()
    {
        return (bool)$this->is_home;
    }

    /**
     * Is the page an alias of a controller/route.
     *
     * @return bool
     */
    public function isAlias()
    {
        return (bool)$this->is_alias;
    }

    /**
     * Get the statuses.
     *
     * @return array
     */
    public function getStatuses()
    {
        return [
            static::INACTIVE => ucfirst(static::INACTIVE),
            static::ACTIVE => ucfirst(static::ACTIVE),
        ];
    }

    /**
     * Is the page active.
     *
     * @return bool
     */
    public function isActive()
    {
        return $this->status === static::ACTIVE;
    }

    /**
     * Is the page inactive.
     *
     * @return bool
     */
    public function isInactive()
    {
        return $this->status === static::INACTIVE;
    }

    /**
     * Get the options for menu display on the frontend.
     *
     * @return array
     */
    public function getMenuDisplayOptions()
    {
        return [
            static::MENU_INVISIBLE => 'No',
            static::MENU_VISIBLE => 'Yes',
        ];
    }

    /**
     * Get the options for program type.
     *
     * @return array
     */
    public function getProgramTypeOptions()
    {
        return [
            static::PROGRAM_ANY => 'Any',
            static::PROGRAM_CASH => 'Cash',
            static::PROGRAM_MERCHANDISE => 'Merchandise',
        ];
    }

    /**
     * Get the meta title.
     *
     * @return string
     */
    public function getMetaTitle()
    {
        $title = (string)$this->meta_title;

        if ($title) {
            return $title;
        }

        return $this->getLabel();
    }

    /**
     * Get the meta description.
     *
     * @return string
     */
    public function getMetaDescription()
    {
        return $this->meta_description;
    }

    /**
     * Get the meta keywords.
     *
     * @return string
     */
    public function getMetaKeywords()
    {
        $keywords = explode(',', $this->meta_keywords ?? '');
        $keywords = array_map('trim', $keywords);

        return implode(',', $keywords);
    }

    /**
     * The position of the page in the menu.
     *
     * @return int
     */
    public function getPosition()
    {
        return (int)$this->position;
    }


    /**
     * Should the page be displayed in the menu.
     *
     * @return bool
     */
    public function isDisplayedInMenu()
    {
        return ! ! $this->display_in_menu;
    }

    /**
     * Check if the page is protected from the public.
     *
     * @return bool
     */
    public function isProtected()
    {
        return $this->is_protected;
    }

    /**
     * Determine if the page should be display on the configured program type.
     *
     * @return bool
     */
    public function isConfiguredProgramType()
    {
        return $this->program_type === Program::getType();
    }

    /**
     * Determine if the page should be display on any program type.
     *
     * @return bool
     */
    public function isAnyProgramType()
    {
        return $this->program_type === static::PROGRAM_ANY;
    }

    /**
     * Can the page be viewed publicly.
     *
     * @return bool
     */
    public function canBeViewed()
    {
        // Allow programmatic override.
        if (! is_null($this->allowCanBeViewed)) {
            return $this->allowCanBeViewed;
        }

        if (! $this->isAnyProgramType() && ! $this->isConfiguredProgramType()) {
            return false;
        }

        if ($this->isProtected()) {
            if (! auth()->check()) {
                return false;
            }
            $groups = array_map(function ($group) {
                return (int)$group;
            }, explode(';', $this->allowed_groups));

            if (! empty($groups)) {
                return auth()->user()->hasGroup($groups);
            }
        }

        return true;
    }

    /**
     * Can the page not be viewed publicly.
     *
     * @return bool
     */
    public function cannotBeViewed()
    {
        return ! $this->canBeViewed();
    }

    /**
     * Check if the provided group ID is allowed to access to page.
     *
     * @param int $groupId
     * @return bool
     */
    public function inAllowedGroups($groupId)
    {
        $groupIds = preg_split('/[,;|\s]+/', trim($this->allowed_groups));

        if (empty($groupIds)) {
            $groupIds = [];
        }

        return in_array($groupId, $groupIds);
    }

    /**
     * Convert the object into something JSON serializable.
     *
     * @return mixed
     */
    public function jsonSerialize(): mixed
    {
        $data = $this->toArray();

        unset($data['content']);

        return $data;
    }
}
