<?php

namespace MintyPHP\Service\Access;

use MintyPHP\Service\Tenant\TenantScopeService;

class UserAuthorizationPolicy implements AuthorizationPolicyInterface
{
    use AuthorizationPolicyContextTrait;

    public const ABILITY_ADMIN_USERS_VIEW = 'admin.users.view';
    public const ABILITY_ADMIN_USERS_CREATE = 'admin.users.create';
    public const ABILITY_ADMIN_USERS_EDIT_CONTEXT = 'admin.users.edit.context';
    public const ABILITY_ADMIN_USERS_EDIT_SUBMIT = 'admin.users.edit.submit';
    public const ABILITY_ADMIN_USERS_ACTIVATE = 'admin.users.activate';
    public const ABILITY_ADMIN_USERS_DEACTIVATE = 'admin.users.deactivate';
    public const ABILITY_ADMIN_USERS_DELETE = 'admin.users.delete';
    public const ABILITY_ADMIN_USERS_BULK = 'admin.users.bulk';
    public const ABILITY_ADMIN_USERS_ACCESS_PDF = 'admin.users.access_pdf';
    public const ABILITY_ADMIN_USERS_ACCESS_PDF_BULK = 'admin.users.access_pdf_bulk';
    public const ABILITY_ADMIN_USERS_AVATAR_UPLOAD = 'admin.users.avatar.upload';
    public const ABILITY_ADMIN_USERS_AVATAR_DELETE = 'admin.users.avatar.delete';
    public const ABILITY_ADMIN_USERS_AVATAR_VIEW = 'admin.users.avatar.view';
    public const ABILITY_ADMIN_USERS_API_TOKENS_MANAGE = 'admin.users.api_tokens_manage';
    public const ABILITY_ADMIN_USERS_SEND_ACCESS = 'admin.users.send_access';
    public const ABILITY_ADMIN_USERS_FORGET_TOKENS = 'admin.users.forget_tokens';
    public const ABILITY_API_USERS_INDEX_GET = 'api.users.index.get';
    public const ABILITY_API_USERS_INDEX_POST = 'api.users.index.post';
    public const ABILITY_API_USERS_SHOW_GET = 'api.users.show.get';
    public const ABILITY_API_USERS_SHOW_UPDATE = 'api.users.show.update';
    public const ABILITY_API_USERS_SHOW_DELETE = 'api.users.show.delete';

    public function __construct(
        private readonly PermissionService $permissionService,
        private readonly TenantScopeService $scopeGateway
    ) {
    }

    public function supports(string $ability): bool
    {
        return in_array($ability, [
            self::ABILITY_ADMIN_USERS_VIEW,
            self::ABILITY_ADMIN_USERS_CREATE,
            self::ABILITY_ADMIN_USERS_EDIT_CONTEXT,
            self::ABILITY_ADMIN_USERS_EDIT_SUBMIT,
            self::ABILITY_ADMIN_USERS_ACTIVATE,
            self::ABILITY_ADMIN_USERS_DEACTIVATE,
            self::ABILITY_ADMIN_USERS_DELETE,
            self::ABILITY_ADMIN_USERS_BULK,
            self::ABILITY_ADMIN_USERS_ACCESS_PDF,
            self::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK,
            self::ABILITY_ADMIN_USERS_AVATAR_UPLOAD,
            self::ABILITY_ADMIN_USERS_AVATAR_DELETE,
            self::ABILITY_ADMIN_USERS_AVATAR_VIEW,
            self::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE,
            self::ABILITY_ADMIN_USERS_SEND_ACCESS,
            self::ABILITY_ADMIN_USERS_FORGET_TOKENS,
            self::ABILITY_API_USERS_INDEX_GET,
            self::ABILITY_API_USERS_INDEX_POST,
            self::ABILITY_API_USERS_SHOW_GET,
            self::ABILITY_API_USERS_SHOW_UPDATE,
            self::ABILITY_API_USERS_SHOW_DELETE,
        ], true);
    }

    public function authorize(string $ability, array $context = []): AuthorizationDecision
    {
        return match ($ability) {
            self::ABILITY_ADMIN_USERS_VIEW => $this->authorizeAdminUsersView($context),
            self::ABILITY_ADMIN_USERS_CREATE => $this->authorizeAdminUsersCreate($context),
            self::ABILITY_ADMIN_USERS_EDIT_CONTEXT => $this->authorizeAdminUsersEditContext($context),
            self::ABILITY_ADMIN_USERS_EDIT_SUBMIT => $this->authorizeAdminUsersEditSubmit($context),
            self::ABILITY_ADMIN_USERS_ACTIVATE,
            self::ABILITY_ADMIN_USERS_DEACTIVATE => $this->authorizeAdminUserStatusUpdate($context),
            self::ABILITY_ADMIN_USERS_DELETE => $this->authorizeAdminUserDelete($context),
            self::ABILITY_ADMIN_USERS_BULK => $this->authorizeAdminUsersBulk($context),
            self::ABILITY_ADMIN_USERS_ACCESS_PDF => $this->authorizeAdminUserAccessPdf($context),
            self::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK => $this->authorizeAdminUsersAccessPdfBulk($context),
            self::ABILITY_ADMIN_USERS_AVATAR_UPLOAD,
            self::ABILITY_ADMIN_USERS_AVATAR_DELETE => $this->authorizeAdminUsersAvatarMutate($context),
            self::ABILITY_ADMIN_USERS_AVATAR_VIEW => $this->authorizeAdminUsersAvatarView($context),
            self::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE => $this->authorizeAdminUsersApiTokensManage($context),
            self::ABILITY_ADMIN_USERS_SEND_ACCESS,
            self::ABILITY_ADMIN_USERS_FORGET_TOKENS => $this->authorizeAdminUserSelfOrUpdate($context),
            self::ABILITY_API_USERS_INDEX_GET => $this->authorizeApiUsersIndexGet($context),
            self::ABILITY_API_USERS_INDEX_POST => $this->authorizeApiUsersIndexPost($context),
            self::ABILITY_API_USERS_SHOW_GET => $this->authorizeApiUsersShow($context, PermissionService::USERS_VIEW),
            self::ABILITY_API_USERS_SHOW_UPDATE => $this->authorizeApiUsersUpdate($context),
            self::ABILITY_API_USERS_SHOW_DELETE => $this->authorizeApiUsersShow($context, PermissionService::USERS_DELETE),
            default => AuthorizationDecision::deny(500, 'authorization_ability_not_supported'),
        };
    }

    private function authorizeAdminUsersView(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::USERS_VIEW, $context, false);
    }

    private function authorizeAdminUsersCreate(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::USERS_CREATE, $context, false);
    }

    // Returns the full capabilities map as attributes — the submit handler re-uses it to avoid recalculating.
    private function authorizeAdminUsersEditContext(array $context): AuthorizationDecision
    {
        return $this->buildEditContextDecision($context);
    }

    private function authorizeAdminUsersEditSubmit(array $context): AuthorizationDecision
    {
        $contextDecision = $this->buildEditContextDecision($context);
        if (!$contextDecision->isAllowed()) {
            return $contextDecision;
        }

        $capabilities = $this->capabilitiesFromDecision($contextDecision);
        if (!($capabilities['can_edit_user'] ?? false)) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        return AuthorizationDecision::allow(['capabilities' => $capabilities]);
    }

    private function authorizeAdminUserStatusUpdate(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::USERS_UPDATE, $context);
    }

    private function authorizeAdminUserDelete(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::USERS_DELETE, $context);
    }

    private function authorizeAdminUserAccessPdf(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::USERS_ACCESS_PDF, $context);
    }

    private function authorizeAdminUsersAccessPdfBulk(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::USERS_ACCESS_PDF, $context, false);
    }

    private function authorizeAdminUsersApiTokensManage(array $context): AuthorizationDecision
    {
        return $this->authorizeAdminUserWithPermission(PermissionService::API_TOKENS_MANAGE, $context);
    }

    private function authorizeAdminUsersAvatarMutate(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        $targetUserId = (int) ($context['target_user_id'] ?? 0);
        if ($actorUserId <= 0 || $targetUserId <= 0) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        $isOwnAccount = $actorUserId === $targetUserId;
        if (!$isOwnAccount && !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
            return AuthorizationDecision::deny(403, 'permission_denied');
        }

        $canEditUser = $this->canEditUserForTarget($actorUserId, $targetUserId);
        if (!$canEditUser) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        return AuthorizationDecision::allow();
    }

    private function authorizeAdminUsersAvatarView(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        $targetUserId = (int) ($context['target_user_id'] ?? 0);
        if ($actorUserId <= 0 || $targetUserId <= 0) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        if ($actorUserId === $targetUserId) {
            return AuthorizationDecision::allow();
        }

        if (!$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
            return AuthorizationDecision::deny(403, 'permission_denied');
        }

        $canEditUser = $this->canEditUserForTarget($actorUserId, $targetUserId);
        if (!$this->hasPermission($actorUserId, PermissionService::USERS_VIEW)
            && !$this->hasPermission($actorUserId, 'address_book.view') // Registered by addressbook module when active
            && !$canEditUser) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        return AuthorizationDecision::allow();
    }

    private function authorizeAdminUserSelfOrUpdate(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        $targetUserId = (int) ($context['target_user_id'] ?? 0);
        if ($targetUserId <= 0) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        if ($this->canEditUserForTarget($actorUserId, $targetUserId)) {
            if ($targetUserId !== $actorUserId
                && !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
                return AuthorizationDecision::deny(403, 'permission_denied');
            }
            return AuthorizationDecision::allow();
        }

        return AuthorizationDecision::deny(403, 'forbidden');
    }

    private function authorizeAdminUsersBulk(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        $action = strtolower(trim((string) ($context['bulk_action'] ?? '')));

        $permission = match ($action) {
            'delete' => PermissionService::USERS_DELETE,
            'activate', 'deactivate', 'send-access' => PermissionService::USERS_UPDATE,
            default => null,
        };

        if ($permission === null) {
            return AuthorizationDecision::deny(400, 'invalid_action');
        }
        if (!$this->hasPermission($actorUserId, $permission)) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        return AuthorizationDecision::allow();
    }

    private function authorizeApiUsersIndexGet(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        if (!$this->hasPermission($actorUserId, PermissionService::USERS_VIEW)) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        return AuthorizationDecision::allow();
    }

    private function authorizeApiUsersIndexPost(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        if (!$this->hasPermission($actorUserId, PermissionService::USERS_CREATE)) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        $input = is_array($context['input'] ?? null) ? $context['input'] : [];
        $scopedTenantId = (int) ($context['scoped_tenant_id'] ?? 0);
        if ($scopedTenantId <= 0) {
            return AuthorizationDecision::allow(['input' => $input]);
        }

        if ($this->containsOutOfScopeTenantId($input['tenant_ids'] ?? [], $scopedTenantId)) {
            return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
        }

        $primaryTenantId = (int) ($input['primary_tenant_id'] ?? 0);
        if ($primaryTenantId > 0 && $primaryTenantId !== $scopedTenantId) {
            return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
        }

        $input['tenant_ids'] = [$scopedTenantId];
        $input['primary_tenant_id'] = $scopedTenantId;

        return AuthorizationDecision::allow(['input' => $input]);
    }

    private function authorizeApiUsersUpdate(array $context): AuthorizationDecision
    {
        $decision = $this->authorizeApiUsersShow($context, PermissionService::USERS_UPDATE);
        if (!$decision->isAllowed()) {
            return $decision;
        }

        $input = is_array($context['input'] ?? null) ? $context['input'] : [];
        $scopedTenantId = (int) ($context['scoped_tenant_id'] ?? 0);
        if ($scopedTenantId <= 0) {
            return AuthorizationDecision::allow(['input' => $input]);
        }

        if (array_key_exists('tenant_ids', $input)
            && $this->containsOutOfScopeTenantId($input['tenant_ids'], $scopedTenantId)) {
            return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
        }

        if (array_key_exists('primary_tenant_id', $input)) {
            $primaryTenantId = (int) ($input['primary_tenant_id'] ?? 0);
            if ($primaryTenantId > 0 && $primaryTenantId !== $scopedTenantId) {
                return AuthorizationDecision::deny(403, 'tenant_scoped_token_forbidden');
            }
        }

        return AuthorizationDecision::allow(['input' => $input]);
    }

    private function authorizeApiUsersShow(array $context, string $permissionKey): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        if (!$this->hasPermission($actorUserId, $permissionKey)) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        $targetUserId = (int) ($context['target_user_id'] ?? 0);
        if ($targetUserId <= 0) {
            return AuthorizationDecision::allow();
        }

        // 404 instead of 403 — don't reveal that the resource exists outside the actor's scope.
        if (!$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
            return AuthorizationDecision::deny(404, 'not_found');
        }

        $scopedTenantId = (int) ($context['scoped_tenant_id'] ?? 0);
        if ($scopedTenantId > 0 && !$this->scopeGateway->resourceBelongsToTenant('users', $targetUserId, $scopedTenantId)) {
            return AuthorizationDecision::deny(404, 'not_found');
        }

        return AuthorizationDecision::allow();
    }

    private function containsOutOfScopeTenantId(mixed $tenantIdsRaw, int $scopedTenantId): bool
    {
        $tenantIds = $this->normalizeTenantIds($tenantIdsRaw);
        if (!$tenantIds) {
            return false;
        }

        return array_diff($tenantIds, [$scopedTenantId]) !== [];
    }

    private function normalizeTenantIds(mixed $tenantIdsRaw): array
    {
        if (!is_array($tenantIdsRaw)) {
            $tenantIdsRaw = [$tenantIdsRaw];
        }

        $tenantIds = [];
        foreach ($tenantIdsRaw as $value) {
            $tenantId = (int) $value;
            if ($tenantId > 0) {
                $tenantIds[$tenantId] = $tenantId;
            }
        }

        return array_values($tenantIds);
    }

    private function canEditUserForTarget(int $actorUserId, int $targetUserId): bool
    {
        if ($actorUserId <= 0 || $targetUserId <= 0) {
            return false;
        }

        $canUpdateUser = $this->hasPermission($actorUserId, PermissionService::USERS_UPDATE);
        if ($targetUserId === $actorUserId) {
            return $canUpdateUser || $this->hasPermission($actorUserId, PermissionService::USERS_SELF_UPDATE);
        }

        return $canUpdateUser;
    }

    private function buildEditContextDecision(array $context): AuthorizationDecision
    {
        $actorUserId = $this->actorUserId($context);
        $targetUserId = (int) ($context['target_user_id'] ?? 0);
        if ($actorUserId <= 0 || $targetUserId <= 0) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        $isOwnAccount = $targetUserId === $actorUserId;
        if (!$isOwnAccount && !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
            return AuthorizationDecision::deny(403, 'permission_denied');
        }

        $canViewUsers = $this->hasPermission($actorUserId, PermissionService::USERS_VIEW);
        $canEditUser = $this->canEditUserForTarget($actorUserId, $targetUserId);
        $canViewPage = $canViewUsers || $canEditUser;
        if (!$canViewPage) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        $canManageTenants = $this->hasPermission($actorUserId, PermissionService::TENANTS_UPDATE);
        $allowedTenantIds = $canManageTenants ? null : $this->scopeGateway->getUserTenantIds($actorUserId);
        $canViewUserAudit = $this->hasPermission($actorUserId, PermissionService::USERS_VIEW_AUDIT);

        $capabilities = [
            'can_view_page' => $canViewPage,
            'can_view_users' => $canViewUsers,
            'can_edit_user' => $canEditUser,
            'can_edit_assignments' => $canEditUser && $this->hasPermission($actorUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS),
            'can_manage_tenants' => $canManageTenants,
            'can_edit_custom_field_values' => $canEditUser && (
                $this->hasPermission($actorUserId, PermissionService::CUSTOM_FIELDS_EDIT_VALUES)
                || ($isOwnAccount && $this->hasPermission($actorUserId, PermissionService::USERS_SELF_UPDATE))
            ),
            'can_manage_api_tokens' => $canEditUser && $this->hasPermission($actorUserId, PermissionService::API_TOKENS_MANAGE),
            'can_view_address_book' => $this->hasPermission($actorUserId, 'address_book.view'), // Registered by addressbook module when active
            'can_view_user_meta' => $this->hasPermission($actorUserId, PermissionService::USERS_VIEW_META),
            'can_view_user_audit' => $canViewUserAudit,
            'can_access_pdf' => $this->hasPermission($actorUserId, PermissionService::USERS_ACCESS_PDF),
            'can_delete_user' => !$isOwnAccount && $this->hasPermission($actorUserId, PermissionService::USERS_DELETE),
            'can_view_security_artifacts' => $isOwnAccount || $canViewUserAudit,
            'can_view_permissions_table' => $this->hasPermission($actorUserId, PermissionService::PERMISSIONS_VIEW),
            'is_own_account' => $isOwnAccount,
            'allowed_tenant_ids' => is_array($allowedTenantIds) ? toIntIds($allowedTenantIds) : null,
        ];

        return AuthorizationDecision::allow(['capabilities' => $capabilities]);
    }

    private function authorizeAdminUserWithPermission(
        string $permissionKey,
        array $context,
        bool $checkTenantScope = true
    ): AuthorizationDecision {
        $actorUserId = $this->actorUserId($context);
        if (!$this->hasPermission($actorUserId, $permissionKey)) {
            return AuthorizationDecision::deny(403, 'forbidden');
        }

        if (!$checkTenantScope) {
            return AuthorizationDecision::allow();
        }

        $targetUserId = (int) ($context['target_user_id'] ?? 0);
        if ($targetUserId > 0
            && $targetUserId !== $actorUserId
            && !$this->scopeGateway->canAccess('users', $targetUserId, $actorUserId)) {
            return AuthorizationDecision::deny(403, 'permission_denied');
        }

        return AuthorizationDecision::allow();
    }
}
