<?php

namespace Ignite\Core\Entities\Filters;

use Ignite\Core\Contracts\Entities\Filters\QueryFilter;
use Ignite\Core\Entities\User;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

abstract class BaseQueryFilter implements QueryFilter
{
    /**
     * Should we prevent all public access (when we have no given or authed user, scope to zero records)?
     *
     * @var bool
     */
    protected $noPublicAccess = true;

    /**
     * Get an instance of the entity this class is responsible for.
     *
     * @return Model
     */
    protected function modelInstance(): Model
    {
        return app($this->modelClass());
    }

    /**
     * {@inheritdoc}
     */
    public function canAccess($model, ?User $user = null): bool
    {
        if (! is_numeric($model)) {
            $this->assertForModelClass($model);
            $model = $model->getKey();
        }

        return $this->apply($this->modelInstance()->newQuery(), null, $user ?? Auth::user())
            ->where($this->modelInstance()->getKeyName(), $model)
            ->exists();
    }

    /**
     * {@inheritdoc}
     */
    public function apply($query, ?string $tableAlias = null, ?User $user = null)
    {
        $user = $user ?? Auth::user();
        $innerAlias = Str::of(class_basename(static::class))->snake();
        $outerTableName = $tableAlias ?? $this->modelInstance()->getTable();

        return $query->whereExists(function ($query) use ($innerAlias, $outerTableName, $user) {
            $this->buildWhereExistsQuery($query, $innerAlias, $outerTableName, $user);
        });
    }

    /**
     * Build the inner exists query that will be used to scope a query.
     *
     * @param QueryBuilder|EloquentBuilder $query
     * @param string $innerAlias
     * @param string $outerTableName
     * @param User|null $user If no user is given, we will assume we have no authed user to work with for this request.
     * @return QueryBuilder|EloquentBuilder
     */
    protected function buildWhereExistsQuery($query, string $innerAlias, string $outerTableName, ?User $user = null)
    {
        $model = $this->modelInstance();
        $query = $query->select(DB::raw(1))
            ->from($model->getTable().' AS '.$innerAlias)
            ->whereColumn(
                $innerAlias.'.'.$model->getKeyName(),
                $outerTableName.'.'.$model->getKeyName()
            );

        if ($this->noPublicAccess && ! $user) {
            $query->whereRaw('1=0');
        }

        return $query;
    }

    /**
     * {@inheritdoc}
     */
    public function assertForModelClass($class): void
    {
        if (! $this->isForModelClass($class)) {
            throw new \LogicException(
                static::class.' is based on the '.$this->modelClass().' class and not the '.$class.' class'
            );
        }
    }

    /**
     * Is the given FQN or model what this query filter is for?
     *
     * @param string|Model $class
     * @return bool
     */
    protected function isForModelClass($class): bool
    {
        return $this->modelClass() === (is_string($class) ? $class : get_class($class));
    }
}
