<?php

namespace App\Services\Auth;

use App\Models\Tenant;
use App\Models\TenantMembership;
use App\Models\User;
use App\Support\Auth\Capabilities;
use App\Support\TenantRole;
use Illuminate\Support\Facades\Log;

/**
 * Capability Resolver
 *
 * Resolves user memberships and capabilities for a given tenant.
 * Caches results per request to avoid N+1 queries.
 */
class CapabilityResolver
{
    private array $resolvedMemberships = [];

    private array $loggedDenials = [];

    /**
     * Get the user's role for a tenant
     */
    public function getRole(User $user, Tenant $tenant): ?TenantRole
    {
        $membership = $this->getMembership($user, $tenant);

        if ($membership === null) {
            return null;
        }

        return TenantRole::tryFrom($membership['role']);
    }

    /**
     * Check if user can perform a capability on a tenant
     */
    public function can(User $user, Tenant $tenant, string $capability): bool
    {
        if (! Capabilities::isKnown($capability)) {
            throw new \InvalidArgumentException("Unknown capability: {$capability}");
        }

        $role = $this->getRole($user, $tenant);

        if ($role === null) {
            $this->logDenial($user, $tenant, $capability);

            return false;
        }

        $allowed = RoleCapabilityMap::hasCapability($role, $capability);

        if (! $allowed) {
            $this->logDenial($user, $tenant, $capability);
        }

        return $allowed;
    }

    private function logDenial(User $user, Tenant $tenant, string $capability): void
    {
        $key = implode(':', [(string) $user->getKey(), (string) $tenant->getKey(), $capability]);

        if (isset($this->loggedDenials[$key])) {
            return;
        }

        $this->loggedDenials[$key] = true;

        Log::warning('rbac.denied', [
            'capability' => $capability,
            'tenant_id' => (int) $tenant->getKey(),
            'actor_user_id' => (int) $user->getKey(),
        ]);
    }

    /**
     * Check if user has any membership for a tenant
     */
    public function isMember(User $user, Tenant $tenant): bool
    {
        return $this->getMembership($user, $tenant) !== null;
    }

    /**
     * Get membership details (cached per request)
     */
    private function getMembership(User $user, Tenant $tenant): ?array
    {
        $cacheKey = "membership_{$user->id}_{$tenant->id}";

        if (! array_key_exists($cacheKey, $this->resolvedMemberships)) {
            $membership = TenantMembership::query()
                ->where('user_id', $user->id)
                ->where('tenant_id', $tenant->id)
                ->first(['role', 'source', 'source_ref']);

            $this->resolvedMemberships[$cacheKey] = $membership?->toArray();
        }

        return $this->resolvedMemberships[$cacheKey];
    }

    /**
     * Prime membership cache for a set of tenants in one query.
     *
     * Used to avoid N+1 queries for bulk selection authorization.
     *
     * @param  array<int, int|string>  $tenantIds
     */
    public function primeMemberships(User $user, array $tenantIds): void
    {
        $tenantIds = array_values(array_unique(array_map(static fn ($id): int => (int) $id, $tenantIds)));

        if ($tenantIds === []) {
            return;
        }

        $missingTenantIds = [];
        foreach ($tenantIds as $tenantId) {
            $cacheKey = "membership_{$user->id}_{$tenantId}";
            if (! array_key_exists($cacheKey, $this->resolvedMemberships)) {
                $missingTenantIds[] = $tenantId;
            }
        }

        if ($missingTenantIds === []) {
            return;
        }

        $memberships = TenantMembership::query()
            ->where('user_id', $user->id)
            ->whereIn('tenant_id', $missingTenantIds)
            ->get(['tenant_id', 'role', 'source', 'source_ref']);

        $byTenantId = $memberships->keyBy('tenant_id');

        foreach ($missingTenantIds as $tenantId) {
            $cacheKey = "membership_{$user->id}_{$tenantId}";
            $membership = $byTenantId->get($tenantId);
            $this->resolvedMemberships[$cacheKey] = $membership?->toArray();
        }
    }

    /**
     * Clear cached memberships (useful for testing or after membership changes)
     */
    public function clearCache(): void
    {
        $this->resolvedMemberships = [];
    }
}
