<?php

declare(strict_types=1);

namespace Builder\Core\Foundation\Concerns;

use Builder\Core\Foundation\Contracts\BuilderAccessible;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;

/**
 * Implements the {@see BuilderAccessible}
 * contract for an Eloquent user model. Permission lookups go through the
 * Spatie/Permission API when present (the standard setup); when the host
 * app hasn't installed Spatie at all, every authenticated user is treated
 * as having full access — the host is in charge of bolting their own
 * permission system on top if they need finer control.
 *
 * @property array<string, mixed>|string|null $widget_preferences
 *
 * @phpstan-require-extends Model
 */
trait HasBuilderAccess
{
    public function canAccessBuilder(): bool
    {
        $permissions = $this->resolveSpatiePermissions();

        if ($permissions === null) {
            return true;
        }

        return $permissions->pluck('name')->contains('read:admin');
    }

    /** @return list<string> */
    public function builderPermissions(): array
    {
        $permissions = $this->resolveSpatiePermissions();

        if ($permissions === null) {
            return ['*'];
        }

        /** @var list<string> $names */
        $names = $permissions->pluck('name')->values()->all();

        return $names;
    }

    /**
     * Returns the list of widget keys the user has chosen to hide.
     *
     * @return list<string>
     */
    public function hiddenWidgets(): array
    {
        $prefs = $this->resolveWidgetPrefs();

        $hidden = $prefs['hidden_widgets'] ?? [];

        return is_array($hidden) ? array_values(array_map('strval', $hidden)) : [];
    }

    /**
     * Toggles a widget's visibility in the user's preferences.
     * If the widget is hidden it will be shown; if shown it will be hidden.
     */
    public function toggleWidget(string $key): void
    {
        $prefs = $this->resolveWidgetPrefs();
        $hidden = $this->hiddenWidgets();

        if (in_array($key, $hidden, true)) {
            $hidden = array_values(array_filter($hidden, fn (string $k): bool => $k !== $key));
        } else {
            $hidden[] = $key;
        }

        $prefs['hidden_widgets'] = $hidden;

        $reflection = new \ReflectionObject($this);

        if ($reflection->hasMethod('update')) {
            $reflection->getMethod('update')->invoke($this, ['widget_preferences' => $prefs]);

            return;
        }

        $this->widget_preferences = $prefs;
    }

    /** @return array<string, mixed> */
    private function resolveWidgetPrefs(): array
    {
        $raw = $this->widget_preferences ?? [];

        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            $raw = is_array($decoded) ? $decoded : [];
        }

        /** @var array<string, mixed> $raw */
        return $raw;
    }

    /**
     * Return the user's permission set when Spatie/laravel-permission (or a
     * compatible shim) is installed on the consuming model. Returns null
     * when no `getAllPermissions()` method is available — in which case
     * the user is treated as having full access.
     *
     * Uses reflection rather than `method_exists($this, ...)` because
     * phpstan eagerly evaluates the latter against each fixture's exact
     * type, flagging both branches as unreachable in one fixture or the
     * other; reflection on a dynamic runtime class hides the lookup.
     *
     * @return EloquentCollection<int, Model&object{name: string}>|null
     */
    private function resolveSpatiePermissions(): ?EloquentCollection
    {
        $reflection = new \ReflectionObject($this);

        if (! $reflection->hasMethod('getAllPermissions')) {
            return null;
        }

        /** @var EloquentCollection<int, Model&object{name: string}> $result */
        $result = $reflection->getMethod('getAllPermissions')->invoke($this);

        return $result;
    }
}
