<?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;

    /**
     * Gives you the model class this scope is for. Providing this allows the filter to determine the class and
     * primary key for y ou.
     *
     * @return string|null
     */
    protected function modelClass(): ?string
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getTable(): string
    {
        if ($modelClass = $this->modelClass()) {
            return app($modelClass)->getTable();
        }

        throw new \RuntimeException('Filter must provide either a modelClass method or a getTable method.');
    }

    /**
     * {@inheritdoc}
     */
    public function getKeyName(): string
    {
        if ($modelClass = $this->modelClass()) {
            return app($modelClass)->getKeyName();
        }

        throw new \RuntimeException('Filter must provide either a modelClass method or a getKeyName method.');
    }

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

        $query = DB::table($this->getTable())->selectRaw(1);
        return $this->apply($query, null, $user ?? Auth::user())
            ->where($this->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->getTable();

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

    /**
     * Initialize the where exists query (add the join between the outer and inner queries and any other pieces before
     * the program logic is added).
     *
     * @param $query
     * @param string $innerAlias
     * @param string $outerTableName
     * @param User|null $user
     * @return mixed
     */
    protected function initWhereExists($query, string $innerAlias, string $outerTableName, ?User $user = null)
    {
        $query = $query->select(DB::raw(1))
            ->from($this->getTable().' AS '.$innerAlias)
            ->whereColumn(
                $innerAlias.'.'.$this->getKeyName(),
                $outerTableName.'.'.$this->getKeyName()
            );

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

        return $query;
    }

    /**
     * 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 buildWhereExists($query, string $innerAlias, string $outerTableName, ?User $user = null)
    {
        return $query;
    }

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

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