<?php

declare(strict_types=1);

namespace App\Services\Auth;

use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\User;
use App\Models\Workspace;
use App\Support\Auth\Capabilities;
use Illuminate\Database\Eloquent\Builder;

final class ManagedEnvironmentAccessScopeResolver
{
    /**
     * @var array<string, array<int, true>|null>
     */
    private array $scopeIdsByUserWorkspace = [];

    /**
     * @var array<int, Workspace|null>
     */
    private array $workspaceById = [];

    public function __construct(
        private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver,
    ) {}

    public function decision(User $user, ManagedEnvironment $tenant, ?string $requiredCapability = null): ManagedEnvironmentAccessDecision
    {
        $tenant = $this->hydrateTenantBoundary($tenant);
        $workspaceId = (int) ($tenant?->workspace_id ?? 0);
        $tenantId = (int) ($tenant?->getKey() ?? 0);

        if (! $tenant instanceof ManagedEnvironment || $workspaceId <= 0 || $tenantId <= 0) {
            return new ManagedEnvironmentAccessDecision(
                workspaceId: $workspaceId,
                managedEnvironmentId: $tenantId,
                userId: (int) $user->getKey(),
                workspaceMember: false,
                workspaceRole: null,
                explicitScopeRowsPresent: false,
                managedEnvironmentAllowed: false,
                failedBoundary: 'managed_environment_scope',
                requiredCapability: $requiredCapability,
                capabilityAllowed: false,
                denialHttpStatus: 404,
            );
        }

        $workspace = $this->workspaceForId($workspaceId);

        if (! $workspace instanceof Workspace) {
            return new ManagedEnvironmentAccessDecision(
                workspaceId: $workspaceId,
                managedEnvironmentId: $tenantId,
                userId: (int) $user->getKey(),
                workspaceMember: false,
                workspaceRole: null,
                explicitScopeRowsPresent: false,
                managedEnvironmentAllowed: false,
                failedBoundary: 'workspace_membership',
                requiredCapability: $requiredCapability,
                capabilityAllowed: false,
                denialHttpStatus: 404,
            );
        }

        $workspaceRole = $this->workspaceCapabilityResolver->getRole($user, $workspace);

        if ($workspaceRole === null) {
            return new ManagedEnvironmentAccessDecision(
                workspaceId: $workspaceId,
                managedEnvironmentId: $tenantId,
                userId: (int) $user->getKey(),
                workspaceMember: false,
                workspaceRole: null,
                explicitScopeRowsPresent: false,
                managedEnvironmentAllowed: false,
                failedBoundary: 'workspace_membership',
                requiredCapability: $requiredCapability,
                capabilityAllowed: false,
                denialHttpStatus: 404,
            );
        }

        $scopeIds = $this->scopeIdsForWorkspace($user, $workspaceId);
        $explicitScopeRowsPresent = $scopeIds !== null;
        $managedEnvironmentAllowed = $scopeIds === null || isset($scopeIds[$tenantId]);

        if (! $managedEnvironmentAllowed) {
            return new ManagedEnvironmentAccessDecision(
                workspaceId: $workspaceId,
                managedEnvironmentId: $tenantId,
                userId: (int) $user->getKey(),
                workspaceMember: true,
                workspaceRole: $workspaceRole->value,
                explicitScopeRowsPresent: true,
                managedEnvironmentAllowed: false,
                failedBoundary: 'managed_environment_scope',
                requiredCapability: $requiredCapability,
                capabilityAllowed: false,
                denialHttpStatus: 404,
            );
        }

        $capabilityAllowed = true;

        if (is_string($requiredCapability) && $requiredCapability !== '') {
            if (! Capabilities::isKnown($requiredCapability)) {
                throw new \InvalidArgumentException("Unknown capability: {$requiredCapability}");
            }

            $capabilityAllowed = WorkspaceRoleCapabilityMap::hasCapability($workspaceRole, $requiredCapability);
        }

        return new ManagedEnvironmentAccessDecision(
            workspaceId: $workspaceId,
            managedEnvironmentId: $tenantId,
            userId: (int) $user->getKey(),
            workspaceMember: true,
            workspaceRole: $workspaceRole->value,
            explicitScopeRowsPresent: $explicitScopeRowsPresent,
            managedEnvironmentAllowed: true,
            failedBoundary: $capabilityAllowed ? null : 'capability',
            requiredCapability: $requiredCapability,
            capabilityAllowed: $capabilityAllowed,
            denialHttpStatus: $capabilityAllowed ? null : 403,
        );
    }

    public function canAccess(User $user, ManagedEnvironment $tenant): bool
    {
        $decision = $this->decision($user, $tenant);

        return $decision->workspaceMember && $decision->managedEnvironmentAllowed;
    }

    /**
     * Returns null when access is inherited across all environments in the workspace.
     *
     * @return array<int, int>|null
     */
    public function allowedManagedEnvironmentIdsForWorkspace(User $user, int $workspaceId): ?array
    {
        $workspace = $this->workspaceForId($workspaceId);

        if (! $workspace instanceof Workspace || $this->workspaceCapabilityResolver->getRole($user, $workspace) === null) {
            return [];
        }

        $scopeIds = $this->scopeIdsForWorkspace($user, $workspaceId);

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

        return array_map('intval', array_keys($scopeIds));
    }

    /**
     * @param  array<int, int|string>  $tenantIds
     */
    public function prime(User $user, array $tenantIds): void
    {
        $tenantIds = array_values(array_unique(array_map(static fn ($id): int => (int) $id, $tenantIds)));

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

        $tenants = ManagedEnvironment::query()
            ->whereIn('id', $tenantIds)
            ->get(['id', 'workspace_id']);

        $workspaceIds = $tenants
            ->pluck('workspace_id')
            ->filter(fn ($id): bool => is_numeric($id))
            ->map(fn ($id): int => (int) $id)
            ->unique()
            ->values()
            ->all();

        $this->workspaceCapabilityResolver->primeMemberships($user, $workspaceIds);

        foreach ($workspaceIds as $workspaceId) {
            $this->scopeIdsForWorkspace($user, $workspaceId);
        }
    }

    public function clearCache(): void
    {
        $this->scopeIdsByUserWorkspace = [];
        $this->workspaceById = [];
    }

    public function applyWorkspaceScopeToQuery(Builder $query, User $user, int $workspaceId, string $qualifiedEnvironmentColumn): Builder
    {
        $allowedIds = $this->allowedManagedEnvironmentIdsForWorkspace($user, $workspaceId);

        if ($allowedIds === []) {
            return $query->whereRaw('1 = 0');
        }

        if ($allowedIds === null) {
            return $query;
        }

        return $query->whereIn($qualifiedEnvironmentColumn, $allowedIds);
    }

    /**
     * Null means inherited access; an array means explicit allowlist.
     *
     * @return array<int, true>|null
     */
    private function scopeIdsForWorkspace(User $user, int $workspaceId): ?array
    {
        $cacheKey = $this->scopeCacheKey($user, $workspaceId);

        if (array_key_exists($cacheKey, $this->scopeIdsByUserWorkspace)) {
            return $this->scopeIdsByUserWorkspace[$cacheKey];
        }

        $scopeIds = ManagedEnvironmentMembership::query()
            ->join('managed_environments', 'managed_environments.id', '=', 'managed_environment_memberships.managed_environment_id')
            ->where('managed_environment_memberships.user_id', (int) $user->getKey())
            ->where('managed_environments.workspace_id', $workspaceId)
            ->pluck('managed_environment_memberships.managed_environment_id')
            ->map(fn ($id): int => (int) $id)
            ->unique()
            ->values();

        $this->scopeIdsByUserWorkspace[$cacheKey] = $scopeIds->isEmpty()
            ? null
            : $scopeIds->mapWithKeys(fn (int $id): array => [$id => true])->all();

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

    private function workspaceForId(int $workspaceId): ?Workspace
    {
        if (! array_key_exists($workspaceId, $this->workspaceById)) {
            $this->workspaceById[$workspaceId] = Workspace::query()->whereKey($workspaceId)->first();
        }

        return $this->workspaceById[$workspaceId];
    }

    private function hydrateTenantBoundary(ManagedEnvironment $tenant): ?ManagedEnvironment
    {
        if ($tenant->exists && $tenant->workspace_id !== null) {
            return $tenant;
        }

        $tenantKey = $tenant->getKey();

        if (! is_numeric($tenantKey)) {
            return null;
        }

        return ManagedEnvironment::query()
            ->withTrashed()
            ->whereKey((int) $tenantKey)
            ->first(['id', 'workspace_id']);
    }

    private function scopeCacheKey(User $user, int $workspaceId): string
    {
        return implode(':', [(string) $user->getKey(), (string) $workspaceId]);
    }
}
