<?php

namespace App\Builders;

use App\Builders\Concerns\CanScopeByUser;
use App\Facades\License;
use App\Models\Artist;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
use LogicException;
use Webmozart\Assert\Assert;

class ArtistBuilder extends FavoriteableBuilder
{
    use CanScopeByUser;

    public const SORT_COLUMNS_NORMALIZE_MAP = [
        'name' => 'artists.name',
        'created_at' => 'artists.created_at',
    ];

    private const VALID_SORT_COLUMNS = [
        'artists.name',
        'artists.created_at',
    ];

    public function onlyStandard(): self
    {
        return $this->whereNotIn('artists.name', [Artist::UNKNOWN_NAME, Artist::VARIOUS_NAME]);
    }

    private function accessible(): self
    {
        if (License::isCommunity()) {
            // With the Community license, all artists are accessible by all users.
            return $this;
        }

        throw_unless($this->user, new LogicException('User must be set to query accessible artists.'));

        if (!$this->user->preferences->includePublicMedia) {
            // If the user does not want to include public media, we only return artists
            // that belong to them.
            return $this->whereBelongsTo($this->user);
        }

        // otherwise, we return artists that belong to the user or
        // artists who have at least one public song owned by the user in the same organization.
        return $this->where(function (Builder $query): void {
            $query->whereBelongsTo($this->user)
                ->orWhereHas('songs', function (Builder $q): void {
                    $q->where('songs.is_public', true)
                        ->whereHas('owner', function (Builder $owner): void {
                            $owner->where('organization_id', $this->user->organization_id)
                                ->where('owner_id', '<>', $this->user->id);
                        });
                });
        });
    }

    private function withPlayCount(bool $includingFavoriteStatus = false): self
    {
        throw_unless($this->user, new LogicException('User must be set to query play counts.'));

        $groupColumns = $includingFavoriteStatus
            ? ['artists.id', 'favorites.created_at']
            : ['artists.id'];

        // As we might have joined the `songs` table already, use an alias for the `songs` table
        // in this join to avoid conflicts.
        return $this->leftJoin('songs as songs_for_playcount', 'artists.id', 'songs_for_playcount.artist_id')
            ->join('interactions', function (JoinClause $join): void {
                $join->on('interactions.song_id', 'songs_for_playcount.id')
                    ->where('interactions.user_id', $this->user->id);
            })
            ->groupBy($groupColumns)
            ->addSelect(DB::raw("SUM(interactions.play_count) as play_count"));
    }

    private static function normalizeSortColumn(string $column): string
    {
        return array_key_exists($column, self::SORT_COLUMNS_NORMALIZE_MAP)
            ? self::SORT_COLUMNS_NORMALIZE_MAP[$column]
            : $column;
    }

    public function sort(string $column, string $direction): self
    {
        $column = self::normalizeSortColumn($column);

        Assert::oneOf($column, self::VALID_SORT_COLUMNS);
        Assert::oneOf(strtolower($direction), ['asc', 'desc']);

        return $this->orderBy($column, $direction);
    }

    public function withUserContext(
        User $user,
        bool $includeFavoriteStatus = true,
        bool $favoritesOnly = false,
        bool $includePlayCount = false,
    ): self {
        $this->user = $user;

        return $this->accessible()
            ->when($includeFavoriteStatus, static fn (self $query) => $query->withFavoriteStatus($favoritesOnly))
            ->when($includePlayCount, static fn (self $query) => $query->withPlayCount($includeFavoriteStatus));
    }
}