<?php

namespace MintyPHP\Service\Access;

use MintyPHP\App\Module\ModuleRegistry;

final class UiAccessService
{
    /** @var array<string, bool> */
    private array $decisionCache = [];

    public function __construct(
        private readonly AuthorizationService $authorizationService,
        private readonly ?ModuleRegistry $moduleRegistry = null
    ) {
    }

    public function allow(int $actorUserId, string $ability, array $context = []): bool
    {
        $ability = trim($ability);
        if ($actorUserId <= 0 || $ability === '') {
            return false;
        }

        // Cache key hashes a sorted context so order-independent inputs produce the same key.
        $normalizedContext = $this->normalizeContext($context);
        $cacheKey = $actorUserId . '|' . $ability . '|' . sha1(json_encode($normalizedContext, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '');
        if (array_key_exists($cacheKey, $this->decisionCache)) {
            return $this->decisionCache[$cacheKey];
        }

        $decision = $this->authorizationService->authorize($ability, [
            'actor_user_id' => $actorUserId,
            ...$normalizedContext,
        ]);

        return $this->decisionCache[$cacheKey] = $decision->isAllowed();
    }

    /**
     * @param array<string, string|array{ability: string, context?: array<string, mixed>}> $map
     * @param array<string, mixed> $baseContext
     * @return array<string, bool>
     */
    public function resolveMap(int $actorUserId, array $map, array $baseContext = []): array
    {
        $resolved = [];
        $normalizedBaseContext = $this->normalizeContext($baseContext);

        foreach ($map as $key => $definition) {
            if (is_string($definition)) {
                $resolved[$key] = $this->allow($actorUserId, $definition, $normalizedBaseContext);
                continue;
            }


            $ability = $definition['ability'];
            $context = [];
            if (array_key_exists('context', $definition)) {
                $context = $this->normalizeContext($definition['context']);
            }
            $resolved[$key] = $this->allow($actorUserId, $ability, [
                ...$normalizedBaseContext,
                ...$context,
            ]);
        }

        return $resolved;
    }

    /**
     * @param array<string, string|array{ability: string, context?: array<string, mixed>}> $map
     * @param array<string, mixed> $baseContext
     * @return array<string, bool>
     */
    public function pageCapabilities(int $actorUserId, array $map, array $baseContext = []): array
    {
        return $this->resolveMap($actorUserId, $map, $baseContext);
    }

    /**
     * Resolve layout capabilities from Core + module-contributed capabilities.
     *
     * @return array<string, bool>
     */
    public function layoutCapabilities(int $actorUserId): array
    {
        $map = UiCapabilityMap::LAYOUT;

        // Merge module UI slot abilities directly into the capability map.
        // Each ability string is used as both key and value (no indirection).
        if ($this->moduleRegistry !== null) {
            try {
                foreach ($this->moduleRegistry->getModuleUiAbilities() as $ability) {
                    $map[$ability] = $ability;
                }
            } catch (\Throwable) {
                // fail-open: no module registry available in this context
            }
        }

        return $this->resolveMap($actorUserId, $map);
    }

    /**
     * @param array<string, mixed> $context
     * @return array<string, mixed>
     */
    private function normalizeContext(array $context): array
    {
        $normalized = $this->normalizeValue($context);
        return $this->normalizeValue($context);
    }

    private function normalizeValue(mixed $value): mixed
    {
        if (!is_array($value)) {
            return $value;
        }

        $isList = array_is_list($value);
        if ($isList) {
            foreach ($value as $idx => $entry) {
                $value[$idx] = $this->normalizeValue($entry);
            }
            return $value;
        }

        ksort($value);
        foreach ($value as $key => $entry) {
            $value[$key] = $this->normalizeValue($entry);
        }
        return $value;
    }
}
