<?php

declare(strict_types=1);

namespace App\Services\EntraAdminRoles;

use App\Models\OperationRun;
use App\Models\StoredReport;
use App\Models\ManagedEnvironment;
use App\Services\Graph\GraphClientInterface;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use App\Support\ProductTelemetry\ProductTelemetryRecorder;
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
use Carbon\CarbonImmutable;
use RuntimeException;

final class EntraAdminRolesReportService
{
    public function __construct(
        private readonly GraphClientInterface $graphClient,
        private readonly HighPrivilegeRoleCatalog $catalog,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
        private readonly ProductTelemetryRecorder $productTelemetryRecorder,
    ) {}

    /**
     * Fetch Graph data and produce a stored report for the given tenant.
     */
    public function generate(ManagedEnvironment $tenant, ?OperationRun $operationRun = null): EntraAdminRolesReportResult
    {
        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);

        $roleDefinitions = $this->fetchRoleDefinitions($graphOptions);
        $roleAssignments = $this->fetchRoleAssignments($graphOptions);

        $payload = $this->buildPayload($roleDefinitions, $roleAssignments);
        $fingerprint = $this->computeFingerprint($roleDefinitions, $roleAssignments);

        $latestReport = StoredReport::query()
            ->where('managed_environment_id', $tenant->getKey())
            ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
            ->orderByDesc('created_at')
            ->orderByDesc('id')
            ->first();

        if ($latestReport instanceof StoredReport && $latestReport->fingerprint === $fingerprint) {
            return new EntraAdminRolesReportResult(
                created: false,
                storedReportId: (int) $latestReport->getKey(),
                fingerprint: $fingerprint,
                payload: $payload,
            );
        }

        $report = StoredReport::create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'report_type' => StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
            'payload' => $payload,
            'fingerprint' => $fingerprint,
            'previous_fingerprint' => $latestReport?->fingerprint,
        ]);

        $this->recordStoredReportTelemetry($report, $operationRun);

        return new EntraAdminRolesReportResult(
            created: true,
            storedReportId: (int) $report->getKey(),
            fingerprint: $fingerprint,
            payload: $payload,
        );
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function fetchRoleDefinitions(array $graphOptions): array
    {
        $response = $this->graphClient->listPolicies('entraRoleDefinitions', $graphOptions);

        if ($response->failed()) {
            throw new RuntimeException('Failed to fetch Entra role definitions from Graph API.');
        }

        return $response->data;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function fetchRoleAssignments(array $graphOptions): array
    {
        $response = $this->graphClient->listPolicies('entraRoleAssignments', array_merge($graphOptions, [
            'expand' => 'principal',
        ]));

        if ($response->failed()) {
            throw new RuntimeException('Failed to fetch Entra role assignments from Graph API.');
        }

        return $response->data;
    }

    /**
     * @return array<string, mixed>
     */
    private function buildPayload(array $roleDefinitions, array $roleAssignments): array
    {
        $roleDefMap = [];

        foreach ($roleDefinitions as $def) {
            $id = (string) ($def['id'] ?? '');

            if ($id !== '') {
                $roleDefMap[$id] = $def;
            }
        }

        $highPrivilegeAssignments = [];

        foreach ($roleAssignments as $assignment) {
            $roleDefId = (string) ($assignment['roleDefinitionId'] ?? '');
            $roleDef = $roleDefMap[$roleDefId] ?? null;
            $templateId = (string) ($roleDef['templateId'] ?? $roleDefId);
            $displayName = $roleDef !== null ? (string) ($roleDef['displayName'] ?? '') : null;

            $severity = $this->catalog->classify($templateId, $displayName);

            if ($severity !== null) {
                $principal = $assignment['principal'] ?? [];

                $highPrivilegeAssignments[] = [
                    'role_definition_id' => $roleDefId,
                    'role_template_id' => (string) ($roleDef['templateId'] ?? ''),
                    'role_display_name' => (string) ($roleDef['displayName'] ?? 'Unknown'),
                    'is_built_in' => (bool) ($roleDef['isBuiltIn'] ?? false),
                    'principal_id' => (string) ($assignment['principalId'] ?? ''),
                    'principal_display_name' => (string) ($principal['displayName'] ?? 'Unknown'),
                    'principal_type' => $this->resolvePrincipalType($principal),
                    'directory_scope_id' => (string) ($assignment['directoryScopeId'] ?? '/'),
                    'severity' => $severity,
                ];
            }
        }

        return [
            'provider_key' => 'microsoft',
            'domain' => 'entra.admin_roles',
            'measured_at' => CarbonImmutable::now('UTC')->toIso8601String(),
            'role_definitions' => $roleDefinitions,
            'role_assignments' => $roleAssignments,
            'totals' => [
                'roles_total' => count($roleDefinitions),
                'assignments_total' => count($roleAssignments),
                'high_privilege_assignments' => count($highPrivilegeAssignments),
            ],
            'high_privilege' => $highPrivilegeAssignments,
        ];
    }

    private function computeFingerprint(array $roleDefinitions, array $roleAssignments): string
    {
        $roleDefMap = [];

        foreach ($roleDefinitions as $def) {
            $id = (string) ($def['id'] ?? '');

            if ($id !== '') {
                $roleDefMap[$id] = $def;
            }
        }

        $tuples = [];

        foreach ($roleAssignments as $assignment) {
            $roleDefId = (string) ($assignment['roleDefinitionId'] ?? '');
            $roleDef = $roleDefMap[$roleDefId] ?? null;
            $templateId = (string) ($roleDef['templateId'] ?? $roleDefId);
            $principalId = (string) ($assignment['principalId'] ?? '');
            $scopeId = (string) ($assignment['directoryScopeId'] ?? '/');

            $tuples[] = "{$templateId}:{$principalId}:{$scopeId}";
        }

        sort($tuples);

        return hash('sha256', implode("\n", $tuples));
    }

    private function resolvePrincipalType(array $principal): string
    {
        $odataType = (string) ($principal['@odata.type'] ?? '');

        return match (true) {
            str_contains($odataType, 'user') => 'user',
            str_contains($odataType, 'group') => 'group',
            str_contains($odataType, 'servicePrincipal') => 'servicePrincipal',
            default => 'unknown',
        };
    }

    private function recordStoredReportTelemetry(StoredReport $report, ?OperationRun $operationRun): void
    {
        if (! $operationRun instanceof OperationRun || ! is_numeric($operationRun->user_id)) {
            return;
        }

        $this->productTelemetryRecorder->record(
            eventName: ProductUsageEventCatalog::STORED_REPORT_CREATED,
            workspaceId: (int) $report->workspace_id,
            tenantId: (int) $report->managed_environment_id,
            userId: (int) $operationRun->user_id,
            subjectType: 'stored_report',
            subjectId: (int) $report->getKey(),
            metadata: [
                'report_type' => $report->report_type,
            ],
            occurredAt: $report->created_at ?? now(),
        );
    }
}
