<?php

namespace App\Services\Intune;

use App\Models\ProviderConnection;
use App\Models\ManagedEnvironment;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphContractRegistry;
use App\Services\Providers\ProviderConnectionResolver;
use App\Services\Providers\ProviderGateway;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\RbacReason;
use Carbon\CarbonImmutable;
use RuntimeException;

class RbacHealthService
{
    public function __construct(
        private readonly GraphClientInterface $graph,
        private readonly GraphContractRegistry $contracts,
        private readonly ?ProviderConnectionResolver $providerConnections = null,
        private readonly ?ProviderGateway $providerGateway = null,
    ) {}

    /**
     * @return array{
     *     status: string,
     *     reason: ?string,
     *     used_artifacts: bool,
     *     reason_translation: array<string, mixed>|null
     * }
     */
    public function check(ManagedEnvironment $tenant): array
    {
        if (! $tenant->isActive()) {
            return $this->record($tenant, 'error', RbacReason::MissingArtifacts->value, false);
        }

        $artifactsPresent = filled($tenant->rbac_group_id) || filled($tenant->rbac_role_assignment_id);

        if (! $artifactsPresent) {
            return $this->record($tenant, 'missing', RbacReason::MissingArtifacts->value, false);
        }

        try {
            $connection = $this->resolveProviderConnection($tenant);
        } catch (RuntimeException) {
            return $this->record($tenant, 'error', RbacReason::ServicePrincipalMissing->value, true);
        }

        $context = $this->providerGateway()->graphOptions($connection);
        $appClientId = is_string($context['client_id'] ?? null) ? (string) $context['client_id'] : null;

        if ($appClientId === null || $appClientId === '') {
            return $this->record($tenant, 'error', RbacReason::ServicePrincipalMissing->value, true);
        }

        $spId = $this->resolveServicePrincipalId($appClientId, $context);
        if (! $spId) {
            return $this->record($tenant, 'error', RbacReason::ServicePrincipalMissing->value, true);
        }

        if ($tenant->rbac_role_assignment_id) {
            $assignmentPath = $this->contracts->rbacRoleAssignmentItemPath($tenant->rbac_role_assignment_id);
            $response = $this->graph->request('GET', $assignmentPath, $context);

            if ($response->successful()) {
                $assignment = $response->data;

                if (! $this->matchesScope($assignment, $tenant)) {
                    return $this->record($tenant, 'partial', RbacReason::ScopeMismatch->value, true);
                }

                if (! $this->matchesRole($assignment, $tenant, $context)) {
                    return $this->record($tenant, 'partial', RbacReason::RoleMismatch->value, true);
                }

                if (! $this->assignmentIncludesGroup($assignment, $tenant)) {
                    return $this->record($tenant, 'partial', RbacReason::ServicePrincipalNotMember->value, true);
                }

                return $this->record($tenant, 'ok', null, true);
            }
        }

        if ($tenant->rbac_group_id) {
            $response = $this->graph->request('GET', "groups/{$tenant->rbac_group_id}", $context);

            if ($response->failed()) {
                return $this->record($tenant, 'error', RbacReason::GroupMissing->value, true);
            }

            if (! $this->groupHasServicePrincipal($tenant->rbac_group_id, $spId, $context)) {
                return $this->record($tenant, 'partial', RbacReason::ServicePrincipalNotMember->value, true);
            }

            // If group exists and SP is a member, but no role assignment found via API,
            // check if this tenant requires manual assignment (unsupported API)
            $hasManualAssignmentWarning = is_array($tenant->rbac_last_warnings)
                && in_array('manual_role_assignment_required', $tenant->rbac_last_warnings, true);

            if ($tenant->rbac_status_reason === RbacReason::ManualAssignmentRequired->value || $hasManualAssignmentWarning) {
                // Keep the manual_assignment_required status - group and membership are OK
                // This account type doesn't support the Intune RBAC API, but permissions work via app registration
                return $this->record($tenant, 'manual_assignment_required', RbacReason::ManualAssignmentRequired->value, true);
            }
        }

        return $this->record($tenant, 'missing', RbacReason::AssignmentMissing->value, true);
    }

    /**
     * @return array{
     *     status: string,
     *     reason: ?string,
     *     used_artifacts: bool,
     *     reason_translation: array<string, mixed>|null
     * }
     */
    private function record(ManagedEnvironment $tenant, string $status, ?string $reason, bool $usedArtifacts): array
    {
        $reasonTranslation = is_string($reason) && $reason !== ''
            ? RbacReason::tryFrom($reason)?->toReasonResolutionEnvelope('detail')->toArray()
            : null;

        $tenant->update([
            'rbac_status' => $status,
            'rbac_status_reason' => $reason,
            'rbac_last_checked_at' => CarbonImmutable::now(),
        ]);

        return [
            'status' => $status,
            'reason' => $reason,
            'used_artifacts' => $usedArtifacts,
            'reason_translation' => $reasonTranslation,
        ];
    }

    private function resolveServicePrincipalId(string $appClientId, array $context): ?string
    {
        $response = $this->graph->request('GET', 'servicePrincipals', [
            'query' => [
                '$filter' => "appId eq '{$appClientId}'",
            ],
        ] + $context);

        return $response->successful()
            ? ($response->data['value'][0]['id'] ?? null)
            : null;
    }

    private function groupHasServicePrincipal(string $groupId, string $spId, array $context): bool
    {
        $response = $this->graph->request('GET', "groups/{$groupId}/members", $context);

        if (! $response->successful()) {
            return false;
        }

        $members = $response->data['value'] ?? [];

        return collect($members)->contains(fn ($member) => ($member['id'] ?? null) === $spId);
    }

    private function matchesScope(array $assignment, ManagedEnvironment $tenant): bool
    {
        $scopes = $assignment['resourceScopes'] ?? [];
        $expected = ($tenant->rbac_scope_mode === 'scope_group' && filled($tenant->rbac_scope_id))
            ? [$tenant->rbac_scope_id]
            : ['/'];

        return $scopes === $expected;
    }

    private function matchesRole(array $assignment, ManagedEnvironment $tenant, array $context): bool
    {
        $roleDefinitionId = data_get($assignment, 'roleDefinition.id') ?? data_get($assignment, 'roleDefinitionId');
        $expectedId = $tenant->rbac_role_definition_id;

        if (! $expectedId || ! $roleDefinitionId) {
            return true;
        }

        return strcasecmp($expectedId, $roleDefinitionId) === 0;
    }

    private function assignmentIncludesGroup(array $assignment, ManagedEnvironment $tenant): bool
    {
        $bindingBase = sprintf('https://graph.microsoft.com/%s', trim(config('graph.version', 'beta'), '/'));
        $expected = "{$bindingBase}/groups/{$tenant->rbac_group_id}";
        $members = $assignment['members@odata.bind'] ?? $assignment['members'] ?? [];

        if (empty($members) && isset($assignment['members@odata.bind'])) {
            $members = $assignment['members@odata.bind'];
        }

        if (isset($assignment['members']) && is_array($assignment['members'])) {
            return collect($assignment['members'])->contains(fn ($id) => $id === $tenant->rbac_group_id);
        }

        return collect($members)->contains($expected);
    }

    private function resolveProviderConnection(ManagedEnvironment $tenant): ProviderConnection
    {
        $resolution = $this->providerConnections()->resolveDefault($tenant, 'microsoft');

        if ($resolution->resolved && $resolution->connection instanceof ProviderConnection) {
            return $resolution->connection;
        }

        $reasonCode = $resolution->effectiveReasonCode();
        $reasonMessage = $resolution->message ?? 'Provider connection is not configured.';

        throw new RuntimeException(sprintf(
            '[%s] %s',
            ProviderReasonCodes::isKnown($reasonCode) ? $reasonCode : ProviderReasonCodes::UnknownError,
            $reasonMessage,
        ));
    }

    private function providerConnections(): ProviderConnectionResolver
    {
        return $this->providerConnections ?? app(ProviderConnectionResolver::class);
    }

    private function providerGateway(): ProviderGateway
    {
        return $this->providerGateway ?? app(ProviderGateway::class);
    }
}
