<?php
namespace App\Services;

use App\Repositories\RbacRepository;
use App\Security\RbacCatalog;

class RbacService
{
    private RbacRepository $repo;
    private array $roleEffectCache = [];
    private array $roleScopeCache = [];
    private array $userEffectCache = [];
    private array $userScopeCache = [];

    public function __construct(RbacRepository $repo)
    {
        $this->repo = $repo;
    }

    public function getCatalog(): array
    {
        $this->repo->syncCatalog(true);
        return [
            'roles' => array_values(array_filter($this->repo->getRoles(true), static fn($role) => (string)($role['role_key'] ?? '') !== 'system')),
            'capabilities' => $this->repo->getCapabilities(false),
            'resources' => RbacCatalog::resources(),
            'scope_types' => RbacCatalog::SCOPE_TYPES,
            'aliases' => RbacCatalog::aliasMap(),
        ];
    }

    public function getRolePermissions(string $roleKey): array
    {
        return [
            'role' => $this->repo->getRoleByKey($roleKey),
            'capabilities' => $this->allowedKeysFromEffects($this->getRoleEffects($roleKey, false)),
            'scopes' => $this->getDataScopesForRole($roleKey, false),
        ];
    }

    public function getUserPermissions(int $userId): array
    {
        return [
            'user_id' => $userId,
            'capability_overrides' => $this->getUserEffects($userId),
            'scopes' => $this->getDataScopesForUser($userId),
        ];
    }

    public function getCurrentPermissions(?int $userId = null, ?string $roleKey = null): array
    {
        $userId = $userId ?? $this->getCurrentUserId();
        $roleKey = $this->normalizeRoleKey($roleKey);
        $capabilityKeys = array_map(static fn($cap) => (string)$cap['key'], RbacCatalog::capabilities());
        $allowed = [];
        foreach ($capabilityKeys as $key) {
            if ($this->can($key, $roleKey, $userId)) {
                $allowed[] = $key;
            }
        }
        $scopes = [];
        foreach (RbacCatalog::resourceKeys() as $resourceKey) {
            $scopes[] = $this->getDataScope($resourceKey, $roleKey, $userId);
        }
        return [
            'role' => $this->repo->getRoleByKey($roleKey),
            'capabilities' => array_values(array_unique($allowed)),
            'scopes' => $scopes,
            'current_role_key' => $roleKey,
            'current_user_id' => $userId,
        ];
    }

    public function saveRole(array $data): array
    {
        $roleKey = trim((string)($data['role_key'] ?? ''));
        if ($roleKey === '') {
            throw new \InvalidArgumentException('role_key الزامی است');
        }
        if (!preg_match('/^[a-zA-Z0-9_\\-]+$/', $roleKey)) {
            throw new \InvalidArgumentException('role_key فقط باید شامل حروف/عدد/خط تیره/آندرلاین باشد');
        }
        $saved = $this->repo->saveRole($data);
        $this->clearRoleCache($roleKey);
        return $saved;
    }

    public function deleteRole(string $roleKey): void
    {
        $roleKey = trim($roleKey);
        if ($roleKey === '') {
            throw new \InvalidArgumentException('role_key الزامی است');
        }
        $this->repo->deleteRole($roleKey);
        $this->clearRoleCache($roleKey);
    }

    public function cloneRoleFromTemplate(string $newRoleKey, string $newTitle, string $templateRoleKey): array
    {
        if (!preg_match('/^[a-zA-Z0-9_\\-]+$/', $newRoleKey)) {
            throw new \InvalidArgumentException('new_role_key نامعتبر است');
        }
        $saved = $this->repo->cloneRoleFromTemplate($newRoleKey, $newTitle, $templateRoleKey);
        $this->clearRoleCache($newRoleKey);
        return $saved;
    }

    public function saveRoleCapabilities(string $roleKey, array $capabilityKeys): void
    {
        $role = $this->repo->getRoleByKey($roleKey);
        if (!$role) throw new \RuntimeException('نقش یافت نشد');

        $capabilityKeys = array_values(array_unique(array_filter(array_map(static fn($v) => trim((string)$v), $capabilityKeys))));
        if (($role['role_key'] ?? '') === 'admin') {
            foreach (['menu.dashboard', 'menu.exports', 'export.catalog', 'users.roles.edit', 'users.scope_options.view'] as $cap) {
                if (!in_array($cap, $capabilityKeys, true)) $capabilityKeys[] = $cap;
            }
        }

        $this->repo->saveRoleCapabilitiesByRoleKey($roleKey, $capabilityKeys);
        $this->clearRoleCache($roleKey);
    }

    public function saveRoleDataScopes(string $roleKey, array $scopes): void
    {
        $this->repo->saveRoleDataScopesByRoleKey($roleKey, $scopes);
        $this->clearRoleCache($roleKey);
    }

    public function saveUserCapabilityOverrides(int $userId, array $allowKeys, array $denyKeys): void
    {
        $this->repo->saveUserCapabilityOverrides($userId, $allowKeys, $denyKeys);
        unset($this->userEffectCache[$userId]);
    }

    public function saveUserDataScopes(int $userId, array $scopes): void
    {
        $this->repo->saveUserDataScopes($userId, $scopes);
        unset($this->userScopeCache[$userId]);
    }

    public function getCurrentRoleKey(): string
    {
        return $this->normalizeRoleKey(null);
    }

    public function getCurrentUserId(): int
    {
        return (int)($_SESSION['user_id'] ?? 0);
    }

    public function can(string $capabilityKey, ?string $roleKey = null, ?int $userId = null): bool
    {
        $roleKey = $this->normalizeRoleKey($roleKey);
        $userId = $userId ?? $this->getCurrentUserId();

        // admin (مدیر عامل) is the deployment's full-access operator role, like the internal
        // `system` role — it bypasses capability checks so a catalog reseed/reconcile can
        // never lock it out of write/manage actions (add lead, payment edit/delete, settings).
        if ($roleKey === 'system' || $roleKey === 'admin') {
            return true;
        }

        $candidates = $this->capabilityCandidates($capabilityKey);
        // Fall back to the code-defined default grants when RBAC is not installed OR
        // installed-but-unseeded (tables exist with no role grants). This prevents a
        // SQL-dump install from locking every non-admin role out of everything; the
        // fallback grants are identical to what the seeder writes.
        if (!$this->repo->isRbacInstalled() || !$this->repo->hasSeededRoleGrants()) {
            foreach ($candidates as $candidate) {
                if ($this->fallbackCan($roleKey, $candidate)) return true;
            }
            return false;
        }

        $userEffects = $userId > 0 ? $this->getUserEffects($userId) : [];
        foreach ($candidates as $candidate) {
            if (($userEffects[$candidate] ?? null) === 'deny') return false;
        }
        foreach ($candidates as $candidate) {
            if (($userEffects[$candidate] ?? null) === 'allow') return true;
        }

        $roleEffects = $this->getRoleEffects($roleKey, true);
        foreach ($candidates as $candidate) {
            if (($roleEffects[$candidate] ?? null) === 'deny') return false;
        }
        foreach ($candidates as $candidate) {
            if (($roleEffects[$candidate] ?? null) === 'allow') return true;
        }

        return false;
    }

    public function requireCapability(string $capabilityKey): void
    {
        if (!$this->can($capabilityKey)) {
            throw new \RuntimeException('Forbidden');
        }
    }

    public function canExportDataset(string $dataset, ?string $roleKey = null): bool
    {
        return $this->can('export.dataset.' . trim($dataset), $roleKey);
    }

    public function canViewMenu(string $menuKey, ?string $roleKey = null): bool
    {
        return $this->can('menu.' . trim($menuKey), $roleKey);
    }

    public function getDataScope(string $resourceKey, ?string $roleKey = null, ?int $userId = null): array
    {
        $resourceKey = trim($resourceKey);
        if (!RbacCatalog::isValidResource($resourceKey)) {
            return ['resource_key' => $resourceKey, 'scope_type' => 'none', 'selected_user_ids' => [], 'source' => 'invalid'];
        }

        $roleKey = $this->normalizeRoleKey($roleKey);
        $userId = $userId ?? $this->getCurrentUserId();
        // Full-access roles see all records.
        if ($roleKey === 'system' || $roleKey === 'admin') {
            return ['resource_key' => $resourceKey, 'scope_type' => 'all', 'selected_user_ids' => [], 'source' => $roleKey];
        }

        foreach ($this->getDataScopesForUser($userId) as $scope) {
            if (($scope['resource_key'] ?? '') === $resourceKey) {
                $scope['source'] = 'user';
                return $this->normalizeResolvedScope($scope, $resourceKey);
            }
        }

        foreach ($this->getDataScopesForRole($roleKey, true) as $scope) {
            if (($scope['resource_key'] ?? '') === $resourceKey) {
                $scope['source'] = 'role';
                return $this->normalizeResolvedScope($scope, $resourceKey);
            }
        }

        if ($this->repo->isRbacInstalled()) {
            $fallback = RbacCatalog::defaultScopeTypeForRole($roleKey);
            return ['resource_key' => $resourceKey, 'scope_type' => $fallback, 'selected_user_ids' => [], 'source' => 'default'];
        }

        $fallback = in_array($roleKey, ['crm_specialist', 'accountant', 'system'], true) ? 'all' : 'self';
        return ['resource_key' => $resourceKey, 'scope_type' => $fallback, 'selected_user_ids' => [], 'source' => 'legacy'];
    }

    public function allowedUserIdsForScope(string $resourceKey, ?int $userId = null): array
    {
        $scope = $this->getDataScope($resourceKey, null, $userId);
        $scopeType = (string)($scope['scope_type'] ?? 'none');
        if ($scopeType === 'all') {
            return [];
        }
        if ($scopeType === 'self') {
            $current = $userId ?? $this->getCurrentUserId();
            return $current > 0 ? [$current] : [];
        }
        if ($scopeType === 'selected_users') {
            return array_values(array_unique(array_filter(array_map('intval', (array)($scope['selected_user_ids'] ?? [])))));
        }
        return [];
    }

    public function allowedDidarIdsForScope(string $resourceKey, ?int $userId = null): array
    {
        $scope = $this->getDataScope($resourceKey, null, $userId);
        $scopeType = (string)($scope['scope_type'] ?? 'none');
        if ($scopeType === 'all') return [];
        if ($scopeType === 'self') {
            $didar = trim((string)($_SESSION['didar_id'] ?? ''));
            return $didar !== '' ? [$didar] : [];
        }
        if ($scopeType === 'selected_users') {
            return $this->repo->getDidarIdsForUserIds((array)($scope['selected_user_ids'] ?? []));
        }
        return [];
    }

    public function applyOwnerScope(array $filters, string $resourceKey, ?string $requestedOwnerDidarId = null): array
    {
        $requestedOwnerDidarId = trim((string)$requestedOwnerDidarId);
        $scope = $this->getDataScope($resourceKey);
        $scopeType = trim((string)($scope['scope_type'] ?? 'none'));

        unset($filters['owner_didar_id'], $filters['owner_didar_ids'], $filters['owner_didar_ids_csv']);

        if ($scopeType === 'all') {
            if ($requestedOwnerDidarId !== '') {
                $filters['owner_didar_id'] = $requestedOwnerDidarId;
            }
            return $filters;
        }

        if ($scopeType === 'self') {
            $self = trim((string)($_SESSION['didar_id'] ?? ''));
            $filters['owner_didar_id'] = $self !== '' ? $self : '__rbac_scope_none__';
            return $filters;
        }

        if ($scopeType === 'selected_users') {
            $allowed = $this->repo->getDidarIdsForUserIds((array)($scope['selected_user_ids'] ?? []));
            if ($requestedOwnerDidarId !== '') {
                $filters['owner_didar_id'] = in_array($requestedOwnerDidarId, $allowed, true)
                    ? $requestedOwnerDidarId
                    : '__rbac_scope_none__';
                return $filters;
            }
            if (empty($allowed)) {
                $filters['owner_didar_ids'] = ['__rbac_scope_none__'];
                $filters['owner_didar_ids_csv'] = '__rbac_scope_none__';
                return $filters;
            }
            if (count($allowed) === 1) {
                $filters['owner_didar_id'] = $allowed[0];
                return $filters;
            }
            $filters['owner_didar_ids'] = $allowed;
            $filters['owner_didar_ids_csv'] = implode(',', $allowed);
            return $filters;
        }

        $filters['owner_didar_id'] = '__rbac_scope_none__';
        return $filters;
    }

    public function assertRecordInScope(string $resourceKey, string $ownerDidarId): void
    {
        if (!$this->isOwnerInScope($resourceKey, $ownerDidarId)) {
            throw new \RuntimeException('Forbidden by RBAC scope');
        }
    }

    public function isOwnerInScope(string $resourceKey, string $ownerDidarId): bool
    {
        $ownerDidarId = trim($ownerDidarId);
        if ($ownerDidarId === '') return false;
        $scope = $this->getDataScope($resourceKey);
        $scopeType = trim((string)($scope['scope_type'] ?? 'none'));
        if ($scopeType === 'all') return true;
        if ($scopeType === 'self') return $ownerDidarId === trim((string)($_SESSION['didar_id'] ?? ''));
        if ($scopeType === 'selected_users') {
            return in_array($ownerDidarId, $this->repo->getDidarIdsForUserIds((array)($scope['selected_user_ids'] ?? [])), true);
        }
        return false;
    }

    private function getRoleEffects(string $roleKey, bool $activeRoleOnly): array
    {
        $cacheKey = $roleKey . ':' . ($activeRoleOnly ? 'active' : 'any');
        if (isset($this->roleEffectCache[$cacheKey])) return $this->roleEffectCache[$cacheKey];

        $effects = [];
        foreach ($this->repo->getRoleCapabilitiesByRoleKey($roleKey, $activeRoleOnly) as $row) {
            $key = (string)($row['capability_key'] ?? '');
            $effect = (string)($row['effect'] ?? 'allow');
            if ($key !== '') $effects[$key] = $effect === 'deny' ? 'deny' : 'allow';
        }
        $this->roleEffectCache[$cacheKey] = $effects;
        return $effects;
    }

    private function getUserEffects(int $userId): array
    {
        if ($userId <= 0) return [];
        if (isset($this->userEffectCache[$userId])) return $this->userEffectCache[$userId];

        $effects = [];
        foreach ($this->repo->getUserCapabilityEffects($userId) as $row) {
            $key = (string)($row['capability_key'] ?? '');
            $effect = (string)($row['effect'] ?? 'allow');
            if ($key !== '') $effects[$key] = $effect === 'deny' ? 'deny' : 'allow';
        }
        $this->userEffectCache[$userId] = $effects;
        return $effects;
    }

    private function getDataScopesForRole(string $roleKey, bool $activeRoleOnly): array
    {
        $cacheKey = $roleKey . ':' . ($activeRoleOnly ? 'active' : 'any');
        if (isset($this->roleScopeCache[$cacheKey])) return $this->roleScopeCache[$cacheKey];
        $rows = $this->repo->getRoleDataScopesByRoleKey($roleKey, $activeRoleOnly);
        $this->roleScopeCache[$cacheKey] = $rows;
        return $rows;
    }

    private function getDataScopesForUser(int $userId): array
    {
        if ($userId <= 0) return [];
        if (isset($this->userScopeCache[$userId])) return $this->userScopeCache[$userId];
        $rows = $this->repo->getUserDataScopes($userId);
        $this->userScopeCache[$userId] = $rows;
        return $rows;
    }

    private function normalizeResolvedScope(array $scope, string $resourceKey): array
    {
        $scopeType = trim((string)($scope['scope_type'] ?? 'none'));
        if (!RbacCatalog::isValidScopeType($scopeType)) {
            $scopeType = 'none';
        }
        $selected = array_values(array_unique(array_filter(array_map('intval', (array)($scope['selected_user_ids'] ?? [])))));
        if ($scopeType === 'selected_users' && empty($this->repo->getDidarIdsForUserIds($selected))) {
            $scopeType = 'none';
            $selected = [];
        }
        if ($scopeType !== 'selected_users') {
            $selected = [];
        }
        $scope['resource_key'] = $resourceKey;
        $scope['scope_type'] = $scopeType;
        $scope['selected_user_ids'] = $selected;
        return $scope;
    }

    private function capabilityCandidates(string $capabilityKey): array
    {
        $capabilityKey = trim($capabilityKey);
        $aliases = RbacCatalog::aliasMap();
        $candidates = [$capabilityKey];
        if (isset($aliases[$capabilityKey])) {
            $candidates[] = $aliases[$capabilityKey];
        }
        foreach ($aliases as $legacy => $canonical) {
            if ($canonical === $capabilityKey) {
                $candidates[] = $legacy;
            }
        }
        foreach ((RbacCatalog::delegatedCapabilityMap()[$capabilityKey] ?? []) as $delegatedCapability) {
            $candidates[] = $delegatedCapability;
        }
        if (in_array($capabilityKey, ['deals.status.edit', 'deals.pipeline.edit', 'deals.course.edit', 'deals.reactivate', 'deals.force_success', 'deals.reconcile'], true)) {
            $candidates[] = 'deals.edit';
            $candidates[] = 'action.deals.edit';
        }
        if (in_array($capabilityKey, ['leads.transfer_owner', 'leads.backfill_products'], true)) {
            $candidates[] = 'leads.edit';
            $candidates[] = 'action.leads.edit';
        }
        foreach (['lookups', 'field_config', 'products'] as $moduleKey) {
            if (in_array($capabilityKey, [$moduleKey . '.create', $moduleKey . '.edit', $moduleKey . '.delete'], true)) {
                $candidates[] = $moduleKey . '.manage';
            }
            if ($capabilityKey === $moduleKey . '.manage') {
                $candidates[] = $moduleKey . '.create';
                $candidates[] = $moduleKey . '.edit';
                $candidates[] = $moduleKey . '.delete';
            }
        }
        if ($capabilityKey === 'products.service_links.manage') {
            $candidates[] = 'products.edit';
            $candidates[] = 'products.manage';
        }
        return array_values(array_unique(array_filter($candidates)));
    }

    private function allowedKeysFromEffects(array $effects): array
    {
        $allowed = [];
        foreach ($effects as $key => $effect) {
            if ($effect === 'allow') $allowed[] = $key;
        }
        return array_values(array_unique($allowed));
    }

    private function normalizeRoleKey(?string $roleKey): string
    {
        $roleKey = trim((string)($roleKey ?? ($_SESSION['role'] ?? 'agent')));
        return $roleKey !== '' ? $roleKey : 'agent';
    }

    private function clearRoleCache(string $roleKey): void
    {
        foreach (array_keys($this->roleEffectCache) as $key) {
            if (strpos($key, $roleKey . ':') === 0) unset($this->roleEffectCache[$key]);
        }
        foreach (array_keys($this->roleScopeCache) as $key) {
            if (strpos($key, $roleKey . ':') === 0) unset($this->roleScopeCache[$key]);
        }
    }

    private function fallbackCan(string $roleKey, string $capabilityKey): bool
    {
        if ($roleKey === 'system' || $roleKey === 'admin') return true;
        $grants = RbacCatalog::defaultGrants();
        return in_array($capabilityKey, $grants[$roleKey] ?? [], true);
    }
}
