<?php

namespace Kylin\Rbac\Traits;

use Illuminate\Support\Collection;
use Kylin\Rbac\Models\Permission;
use Kylin\Rbac\Models\Role;
use Kylin\Rbac\Services\PermissionChangeNotifier;

/**
 * 角色关联 Trait
 *
 * 为模型添加角色和权限的关联关系。
 * 使用此 Trait 的模型可通过 roles() 获取关联角色。
 */
trait HasRoles
{
    public function roles()
    {
        return $this->morphToMany(
            Role::class,
            'model',
            config('kylin-rbac.table_names.model_has_roles', 'kylin_model_has_roles'),
            'model_id',
            'role_id',
        );
    }

    public function permissions()
    {
        return $this->morphToMany(
            Permission::class,
            'model',
            config('kylin-rbac.table_names.model_has_permissions', 'kylin_model_has_permissions'),
            'model_id',
            'permission_id',
        );
    }

    public function assignRole(...$roles): self
    {
        $roleIds = collect($roles)
            ->flatten()
            ->map(fn ($role) => $this->getStoredRole($role))
            ->filter()
            ->pluck('id');

        $this->roles()->syncWithoutDetaching($roleIds);
        $this->clearRbacCache();

        // 依据等保 2.0 (GB/T 22239) 8.1.4.6：权限变更审计
        $this->auditRbacChange('role_assign', ['roles' => $roleIds->toArray()]);

        // 敏感角色变更通知
        $roles = collect($roles)->flatten();
        foreach ($roles as $role) {
            PermissionChangeNotifier::notifyRoleChange('assign', $this, $this->getStoredRole($role));
        }

        return $this;
    }

    public function removeRole(...$roles): self
    {
        $roleIds = collect($roles)
            ->flatten()
            ->map(fn ($role) => $this->getStoredRole($role))
            ->filter()
            ->pluck('id');

        $this->roles()->detach($roleIds);
        $this->clearRbacCache();

        // 依据等保 2.0 (GB/T 22239) 8.1.4.6：权限变更审计
        $this->auditRbacChange('role_remove', ['roles' => $roleIds->toArray()]);

        // 敏感角色变更通知
        $roles = collect($roles)->flatten();
        foreach ($roles as $role) {
            PermissionChangeNotifier::notifyRoleChange('remove', $this, $this->getStoredRole($role));
        }

        return $this;
    }

    public function givePermissionTo(...$permissions): self
    {
        $permissionIds = collect($permissions)
            ->flatten()
            ->map(fn ($perm) => $this->getStoredPermission($perm))
            ->filter()
            ->pluck('id');

        $this->permissions()->syncWithoutDetaching($permissionIds);
        $this->clearRbacCache();

        // 依据等保 2.0 (GB/T 22239) 8.1.4.6：权限变更审计
        $this->auditRbacChange('permission_grant', ['permissions' => $permissionIds->toArray()]);

        // 敏感权限变更通知
        $permissionNames = collect($permissions)->flatten()
            ->map(fn ($p) => $this->getStoredPermission($p))
            ->filter()
            ->pluck('name');
        foreach ($permissionNames as $permName) {
            PermissionChangeNotifier::notifyPermissionChange('grant', $this, $permName);
        }

        return $this;
    }

    public function revokePermissionTo(...$permissions): self
    {
        $permissionIds = collect($permissions)
            ->flatten()
            ->map(fn ($perm) => $this->getStoredPermission($perm))
            ->filter()
            ->pluck('id');

        $this->permissions()->detach($permissionIds);
        $this->clearRbacCache();

        // 依据等保 2.0 (GB/T 22239) 8.1.4.6：权限变更审计
        $this->auditRbacChange('permission_revoke', ['permissions' => $permissionIds->toArray()]);

        // 敏感权限变更通知
        $permissionNames = collect($permissions)->flatten()
            ->map(fn ($p) => $this->getStoredPermission($p))
            ->filter()
            ->pluck('name');
        foreach ($permissionNames as $permName) {
            PermissionChangeNotifier::notifyPermissionChange('revoke', $this, $permName);
        }

        return $this;
    }

    public function hasRole(string|array $roles, ?string $guard = null): bool
    {
        $guard = $guard ?? config('kylin-rbac.guard', 'web');
        $roles = collect(is_array($roles) ? $roles : [$roles]);

        return $this->roles
            ->where('guard_name', $guard)
            ->pluck('name')
            ->intersect($roles)
            ->isNotEmpty();
    }

    public function hasPermissionTo(string $permission, ?string $guard = null): bool
    {
        $guard = $guard ?? config('kylin-rbac.guard', 'web');

        return $this->getAllPermissions($guard)
            ->contains('name', $permission);
    }

    public function getAllPermissions(?string $guard = null): Collection
    {
        $guard = $guard ?? config('kylin-rbac.guard', 'web');

        $directPermissions = $this->permissions->where('guard_name', $guard);
        $rolePermissions = $this->roles
            ->where('guard_name', $guard)
            ->flatMap(fn ($role) => $role->permissions);

        return $directPermissions->merge($rolePermissions)->unique('id');
    }

    private function getStoredRole(mixed $role): ?Role
    {
        if ($role instanceof Role) {
            return $role;
        }
        return Role::findByName($role);
    }

    private function getStoredPermission(mixed $permission): ?Permission
    {
        if ($permission instanceof Permission) {
            return $permission;
        }
        return Permission::findByName($permission);
    }

    private function clearRbacCache(): void
    {
        if (config('kylin-rbac.cache.enabled', true)) {
            $key = config('kylin-rbac.cache.key', 'kylin.rbac');
            \Illuminate\Support\Facades\Cache::forget("{$key}.user.{$this->getKey()}");
        }
    }

    /**
     * 审计 RBAC 权限变更
     *
     * 依据等保 2.0 (GB/T 22239) 8.1.4.6：
     * 所有权限变更操作（角色分配、权限变更）需记录审计日志。
     *
     * 集成 kylin/audit-log 包，若未安装则静默跳过。
     */
    private function auditRbacChange(string $event, array $payload): void
    {
        if (! config('kylin-rbac.audit.enabled', true)) {
            return;
        }

        if (! class_exists(\Kylin\AuditLog\Models\AuditLog::class)) {
            return;
        }

        try {
            \Kylin\AuditLog\Models\AuditLog::create([
                'event'      => $event,
                'model_type' => get_class($this),
                'model_id'   => $this->getKey(),
                'old_values' => null,
                'new_values' => $payload,
                'user_id'    => auth()->id(),
                'ip'         => request()->ip(),
                'user_agent' => request()->userAgent(),
                'url'        => request()->fullUrl(),
                'tag'        => 'rbac',
            ]);
        } catch (\Throwable $e) {
            logger()->warning('RBAC 审计日志写入失败', [
                'event'   => $event,
                'model'   => get_class($this),
                'model_id' => $this->getKey(),
                'error'   => $e->getMessage(),
            ]);
        }
    }
}
