<?php

namespace App\Services\Directory;

use App\Jobs\SyncRoleDefinitionsJob;
use App\Models\EntraRoleDefinition;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphContractRegistry;
use App\Services\Graph\GraphResponse;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use App\Services\Providers\ProviderOperationStartGate;
use App\Services\Providers\ProviderOperationStartResult;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunType;
use Carbon\CarbonImmutable;

class RoleDefinitionsSyncService
{
    public function __construct(
        private readonly GraphClientInterface $graph,
        private readonly GraphContractRegistry $contracts,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
        private readonly ProviderOperationStartGate $providerStarts,
    ) {}

    public function startManualSync(ManagedEnvironment $tenant, User $user): ProviderOperationStartResult
    {
        $selectionKey = 'role_definitions_v1';

        return $this->providerStarts->start(
            tenant: $tenant,
            connection: null,
            operationType: OperationRunType::DirectoryRoleDefinitionsSync->value,
            dispatcher: function (OperationRun $run) use ($tenant): void {
                $providerConnectionId = is_numeric($run->context['provider_connection_id'] ?? null)
                    ? (int) $run->context['provider_connection_id']
                    : null;

                SyncRoleDefinitionsJob::dispatch(
                    tenantId: (int) $tenant->getKey(),
                    providerConnectionId: $providerConnectionId,
                    operationRun: $run,
                )->afterCommit();
            },
            initiator: $user,
            extraContext: [
                'selection_key' => $selectionKey,
                'trigger' => 'manual',
                'required_capability' => Capabilities::TENANT_MANAGE,
            ],
        );
    }

    /**
     * @return array{
     *   pages_fetched:int,
     *   items_observed_count:int,
     *   items_upserted_count:int,
     *   error_count:int,
     *   safety_stop_triggered:bool,
     *   safety_stop_reason:?string,
     *   error_code:?string,
     *   error_category:?string,
     *   error_summary:?string
     * }
     */
    public function sync(ManagedEnvironment $tenant, ?int $providerConnectionId = null): array
    {
        $nowUtc = CarbonImmutable::now('UTC');

        $policyType = $this->contracts->directoryRoleDefinitionsPolicyType();
        $path = $this->contracts->directoryRoleDefinitionsListPath();

        $contract = $this->contracts->get($policyType);
        $query = [];

        if (isset($contract['allowed_select']) && is_array($contract['allowed_select']) && $contract['allowed_select'] !== []) {
            $query['$select'] = $contract['allowed_select'];
        }

        $pageSize = (int) config('directory_role_definitions.page_size', 200);
        if ($pageSize > 0) {
            $query['$top'] = $pageSize;
        }

        $sanitized = $this->contracts->sanitizeQuery($policyType, $query);
        $query = $sanitized['query'];

        $maxPages = (int) config('directory_role_definitions.safety_stop.max_pages', 50);
        $maxRuntimeSeconds = (int) config('directory_role_definitions.safety_stop.max_runtime_seconds', 120);
        $deadline = $nowUtc->addSeconds(max(1, $maxRuntimeSeconds));

        $pagesFetched = 0;
        $observed = 0;
        $upserted = 0;

        $safetyStopTriggered = false;
        $safetyStopReason = null;

        $errorCode = null;
        $errorCategory = null;
        $errorSummary = null;
        $errorCount = 0;

        $options = $providerConnectionId !== null
            ? $this->graphOptionsResolver->resolveForConnection($tenant, $providerConnectionId)
            : $this->graphOptionsResolver->resolveForTenant($tenant);
        $useQuery = $query;
        $nextPath = $path;

        while ($nextPath) {
            if (CarbonImmutable::now('UTC')->greaterThan($deadline)) {
                $safetyStopTriggered = true;
                $safetyStopReason = 'runtime_exceeded';
                break;
            }

            if ($pagesFetched >= $maxPages) {
                $safetyStopTriggered = true;
                $safetyStopReason = 'max_pages_exceeded';
                break;
            }

            $response = $this->requestWithRetry('GET', $nextPath, $options + ['query' => $useQuery]);

            if ($response->failed()) {
                [$errorCode, $errorCategory, $errorSummary] = $this->categorizeError($response);
                $errorCount = 1;
                break;
            }

            $pagesFetched++;

            $data = $response->data;
            $pageItems = $data['value'] ?? (is_array($data) ? $data : []);

            if (is_array($pageItems)) {
                foreach ($pageItems as $item) {
                    if (! is_array($item)) {
                        continue;
                    }

                    $entraId = $item['id'] ?? null;
                    if (! is_string($entraId) || $entraId === '') {
                        continue;
                    }

                    $displayName = $item['displayName'] ?? null;
                    $isBuiltIn = (bool) ($item['isBuiltIn'] ?? false);

                    $values = [
                        'workspace_id' => $tenant->workspace_id,
                        'display_name' => is_string($displayName) ? $displayName : $entraId,
                        'is_built_in' => $isBuiltIn,
                        'last_seen_at' => $nowUtc,
                    ];

                    EntraRoleDefinition::query()->updateOrCreate([
                        'managed_environment_id' => $tenant->getKey(),
                        'entra_id' => $entraId,
                    ], $values);

                    $observed++;
                    $upserted++;
                }
            }

            $nextLink = is_array($data) ? ($data['@odata.nextLink'] ?? null) : null;

            if (! is_string($nextLink) || $nextLink === '') {
                break;
            }

            $nextPath = $this->stripGraphBaseUrl($nextLink);
            $useQuery = [];
        }

        $retentionDays = (int) config('directory_role_definitions.retention_days', 90);
        if ($retentionDays > 0) {
            $cutoff = $nowUtc->subDays($retentionDays);

            EntraRoleDefinition::query()
                ->where('managed_environment_id', $tenant->getKey())
                ->whereNotNull('last_seen_at')
                ->where('last_seen_at', '<', $cutoff)
                ->delete();
        }

        return [
            'pages_fetched' => $pagesFetched,
            'items_observed_count' => $observed,
            'items_upserted_count' => $upserted,
            'error_count' => $errorCount,
            'safety_stop_triggered' => $safetyStopTriggered,
            'safety_stop_reason' => $safetyStopReason,
            'error_code' => $errorCode,
            'error_category' => $errorCategory,
            'error_summary' => $errorSummary,
        ];
    }

    private function requestWithRetry(string $method, string $path, array $options): GraphResponse
    {
        $maxRetries = (int) config('directory_role_definitions.safety_stop.max_retries', 6);
        $maxRetries = max(0, $maxRetries);

        for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
            $response = $this->graph->request($method, $path, $options);

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

            $status = (int) ($response->status ?? 0);

            if (! in_array($status, [429, 503], true) || $attempt >= $maxRetries) {
                return $response;
            }

            $baseDelaySeconds = min(30, 1 << $attempt);
            $jitterMillis = random_int(0, 250);
            usleep(($baseDelaySeconds * 1000 + $jitterMillis) * 1000);
        }

        return new GraphResponse(success: false, data: [], status: 500, errors: [['message' => 'Retry loop exceeded']]);
    }

    /**
     * @return array{0:string,1:string,2:string}
     */
    private function categorizeError(GraphResponse $response): array
    {
        $status = (int) ($response->status ?? 0);

        if (in_array($status, [401, 403], true)) {
            return ['permission_denied', 'permission', 'Graph permission denied for role definitions listing.'];
        }

        if ($status === 429) {
            return ['throttled', 'throttling', 'Graph throttled the role definitions listing request.'];
        }

        if (in_array($status, [500, 502, 503, 504], true)) {
            return ['graph_unavailable', 'transient', 'Graph returned a transient server error.'];
        }

        return ['graph_request_failed', 'unknown', 'Graph request failed.'];
    }

    private function stripGraphBaseUrl(string $nextLink): string
    {
        $base = rtrim((string) config('graph.base_url', 'https://graph.microsoft.com'), '/')
            .'/'.trim((string) config('graph.version', 'v1.0'), '/');

        if (str_starts_with($nextLink, $base)) {
            return ltrim((string) substr($nextLink, strlen($base)), '/');
        }

        return ltrim($nextLink, '/');
    }
}
