<?php

namespace App\Domain\Identity\Services;

use App\Domain\Identity\Enums\PermissionScope;
use App\Domain\Identity\Models\Role;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\PermissionRegistrar;

/**
 * Create / update / clone roles together with their scoped grants (§66A).
 *
 * Grants arrive as `permission-key => scope`; unknown keys are rejected by the
 * form request, so anything reaching here is already validated.
 */
class RoleService
{
    public function __construct(private readonly PermissionScopeResolver $resolver) {}

    /** @param array<string, string> $grants */
    public function create(array $attributes, array $grants): Role
    {
        return DB::transaction(function () use ($attributes, $grants) {
            $role = Role::create([
                'name' => $attributes['name'] ?? Str::slug($attributes['label']),
                'guard_name' => 'web',
                'label' => $attributes['label'],
                'description' => $attributes['description'] ?? null,
                'level' => $attributes['level'] ?? 100,
                'is_system' => false,
                'is_active' => $attributes['is_active'] ?? true,
            ]);

            $this->syncGrants($role, $grants);

            return $role;
        });
    }

    /** @param array<string, string>|null $grants null leaves grants untouched. */
    public function update(Role $role, array $attributes, ?array $grants = null): Role
    {
        return DB::transaction(function () use ($role, $attributes, $grants) {
            $role->fill(array_filter([
                'label' => $attributes['label'] ?? null,
                'description' => $attributes['description'] ?? null,
                'level' => $attributes['level'] ?? null,
            ], fn ($value) => $value !== null));

            if (array_key_exists('is_active', $attributes)) {
                $role->is_active = (bool) $attributes['is_active'];
            }

            $role->save();

            if ($grants !== null) {
                $this->syncGrants($role, $grants);
            }

            return $role;
        });
    }

    public function clone(Role $source, string $label, ?string $name = null): Role
    {
        return $this->create([
            'name' => $name ?: $this->uniqueName($label),
            'label' => $label,
            'description' => $source->description,
            'level' => $source->level,
            'is_active' => true,
        ], $this->grantsFor($source));
    }

    public function delete(Role $role): void
    {
        DB::transaction(function () use ($role) {
            $role->permissionScopes()->delete();
            $role->delete();
        });

        $this->flush();
    }

    /** @return array<string, string> permission name => scope */
    public function grantsFor(Role $role): array
    {
        return $role->permissionScopes()
            ->with('permission:id,name')
            ->get()
            ->filter(fn ($scope) => $scope->permission !== null)
            ->mapWithKeys(fn ($scope) => [$scope->permission->name => $scope->scope->value])
            ->all();
    }

    /** @param array<string, string> $grants */
    private function syncGrants(Role $role, array $grants): void
    {
        $permissionIds = Permission::where('guard_name', $role->guard_name)
            ->whereIn('name', array_keys($grants))
            ->pluck('id', 'name');

        $role->syncPermissions($permissionIds->keys()->all());

        $rows = [];
        foreach ($grants as $permission => $scope) {
            if (! isset($permissionIds[$permission])) {
                continue;
            }

            $rows[] = [
                'role_id' => $role->id,
                'permission_id' => $permissionIds[$permission],
                'scope' => PermissionScope::from($scope)->value,
                'created_at' => now(),
                'updated_at' => now(),
            ];
        }

        DB::table('role_permission_scopes')->where('role_id', $role->id)->delete();
        foreach (array_chunk($rows, 500) as $chunk) {
            DB::table('role_permission_scopes')->insert($chunk);
        }

        $this->flush();
    }

    private function uniqueName(string $label): string
    {
        $base = Str::slug($label);
        $name = $base;
        $suffix = 2;

        while (Role::where('name', $name)->exists()) {
            $name = "{$base}-{$suffix}";
            $suffix++;
        }

        return $name;
    }

    private function flush(): void
    {
        $this->resolver->forget();
        app(PermissionRegistrar::class)->forgetCachedPermissions();
    }
}
