<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Jobs\Middleware\TrackOperationRun;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineSnapshotItem;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Baselines\BaselineAutoCloseService;
use App\Services\Baselines\BaselineContentCapturePhase;
use App\Services\Baselines\BaselineSnapshotIdentity;
use App\Services\Baselines\CurrentStateHashResolver;
use App\Services\Baselines\Evidence\BaselinePolicyVersionResolver;
use App\Services\Baselines\Evidence\ContentEvidenceProvider;
use App\Services\Baselines\Evidence\EvidenceProvenance;
use App\Services\Baselines\Evidence\MetaEvidenceProvider;
use App\Services\Baselines\Evidence\ResolvedEvidence;
use App\Services\Drift\DriftHasher;
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
use App\Services\Drift\Normalizers\SettingsNormalizer;
use App\Services\Findings\FindingSlaPolicy;
use App\Services\Intune\AuditLogger;
use App\Services\Intune\IntuneRoleDefinitionNormalizer;
use App\Services\OperationRunService;
use App\Services\Settings\SettingsResolver;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineCompareReasonCode;
use App\Support\Baselines\BaselineFullContentRolloutGate;
use App\Support\Baselines\BaselineScope;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\Baselines\PolicyVersionCapturePurpose;
use App\Support\Inventory\InventoryCoverage;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Carbon\CarbonImmutable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;

class CompareBaselineToTenantJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * @var array<int, string>
     */
    private array $baselineContentHashCache = [];

    public ?OperationRun $operationRun = null;

    public function __construct(
        public OperationRun $run,
    ) {
        $this->operationRun = $run;
    }

    /**
     * @return array<int, object>
     */
    public function middleware(): array
    {
        return [new TrackOperationRun];
    }

    public function handle(
        BaselineSnapshotIdentity $snapshotIdentity,
        AuditLogger $auditLogger,
        OperationRunService $operationRunService,
        ?SettingsResolver $settingsResolver = null,
        ?BaselineAutoCloseService $baselineAutoCloseService = null,
        ?CurrentStateHashResolver $hashResolver = null,
        ?MetaEvidenceProvider $metaEvidenceProvider = null,
        ?BaselineContentCapturePhase $contentCapturePhase = null,
        ?BaselineFullContentRolloutGate $rolloutGate = null,
        ?ContentEvidenceProvider $contentEvidenceProvider = null,
    ): void {
        $settingsResolver ??= app(SettingsResolver::class);
        $baselineAutoCloseService ??= app(BaselineAutoCloseService::class);
        $hashResolver ??= app(CurrentStateHashResolver::class);
        $metaEvidenceProvider ??= app(MetaEvidenceProvider::class);
        $contentCapturePhase ??= app(BaselineContentCapturePhase::class);
        $rolloutGate ??= app(BaselineFullContentRolloutGate::class);
        $contentEvidenceProvider ??= app(ContentEvidenceProvider::class);

        if (! $this->operationRun instanceof OperationRun) {
            $this->fail(new RuntimeException('OperationRun context is required for CompareBaselineToTenantJob.'));

            return;
        }

        $context = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $profileId = (int) ($context['baseline_profile_id'] ?? 0);
        $snapshotId = (int) ($context['baseline_snapshot_id'] ?? 0);

        $profile = BaselineProfile::query()->find($profileId);

        if (! $profile instanceof BaselineProfile) {
            throw new RuntimeException("BaselineProfile #{$profileId} not found.");
        }

        $tenant = Tenant::query()->find($this->operationRun->tenant_id);

        if (! $tenant instanceof Tenant) {
            throw new RuntimeException("Tenant #{$this->operationRun->tenant_id} not found.");
        }

        $workspace = Workspace::query()->whereKey((int) $tenant->workspace_id)->first();

        if (! $workspace instanceof Workspace) {
            throw new RuntimeException("Workspace #{$tenant->workspace_id} not found.");
        }

        $initiator = $this->operationRun->user_id
            ? User::query()->find($this->operationRun->user_id)
            : null;

        $effectiveScope = BaselineScope::fromJsonb($context['effective_scope'] ?? null);
        $effectiveTypes = $effectiveScope->allTypes();
        $scopeKey = 'baseline_profile:'.$profile->getKey();

        $captureMode = $profile->capture_mode instanceof BaselineCaptureMode
            ? $profile->capture_mode
            : BaselineCaptureMode::Opportunistic;

        if ($captureMode === BaselineCaptureMode::FullContent) {
            try {
                $rolloutGate->assertEnabled();
            } catch (RuntimeException) {
                $this->auditStarted(
                    auditLogger: $auditLogger,
                    tenant: $tenant,
                    profile: $profile,
                    initiator: $initiator,
                    captureMode: $captureMode,
                    subjectsTotal: 0,
                    effectiveScope: $effectiveScope,
                );

                $effectiveTypeCount = count($effectiveTypes);
                $gapCount = max(1, $effectiveTypeCount);

                $this->completeWithCoverageWarning(
                    operationRunService: $operationRunService,
                    auditLogger: $auditLogger,
                    tenant: $tenant,
                    profile: $profile,
                    initiator: $initiator,
                    inventorySyncRun: null,
                    coverageProof: false,
                    effectiveTypes: $effectiveTypes,
                    coveredTypes: [],
                    uncoveredTypes: $effectiveTypes,
                    errorsRecorded: $gapCount,
                    captureMode: $captureMode,
                    reasonCode: BaselineCompareReasonCode::RolloutDisabled,
                    evidenceGapsByReason: [
                        BaselineCompareReasonCode::RolloutDisabled->value => $gapCount,
                    ],
                );

                return;
            }
        }

        if ($effectiveTypes === []) {
            $this->auditStarted(
                auditLogger: $auditLogger,
                tenant: $tenant,
                profile: $profile,
                initiator: $initiator,
                captureMode: $captureMode,
                subjectsTotal: 0,
                effectiveScope: $effectiveScope,
            );

            $this->completeWithCoverageWarning(
                operationRunService: $operationRunService,
                auditLogger: $auditLogger,
                tenant: $tenant,
                profile: $profile,
                initiator: $initiator,
                inventorySyncRun: null,
                coverageProof: false,
                effectiveTypes: [],
                coveredTypes: [],
                uncoveredTypes: [],
                errorsRecorded: 1,
                captureMode: $captureMode,
                reasonCode: BaselineCompareReasonCode::NoSubjectsInScope,
                evidenceGapsByReason: [],
            );

            return;
        }

        $inventorySyncRun = $this->resolveLatestInventorySyncRun($tenant);
        $coverage = $inventorySyncRun instanceof OperationRun
            ? InventoryCoverage::fromContext($inventorySyncRun->context)
            : null;

        if (! $inventorySyncRun instanceof OperationRun || ! $coverage instanceof InventoryCoverage) {
            $this->auditStarted(
                auditLogger: $auditLogger,
                tenant: $tenant,
                profile: $profile,
                initiator: $initiator,
                captureMode: $captureMode,
                subjectsTotal: 0,
                effectiveScope: $effectiveScope,
            );

            $this->completeWithCoverageWarning(
                operationRunService: $operationRunService,
                auditLogger: $auditLogger,
                tenant: $tenant,
                profile: $profile,
                initiator: $initiator,
                inventorySyncRun: $inventorySyncRun,
                coverageProof: false,
                effectiveTypes: $effectiveTypes,
                coveredTypes: [],
                uncoveredTypes: $effectiveTypes,
                errorsRecorded: count($effectiveTypes),
                captureMode: $captureMode,
            );

            return;
        }

        $coveredTypes = array_values(array_intersect($effectiveTypes, $coverage->coveredTypes()));
        $uncoveredTypes = array_values(array_diff($effectiveTypes, $coveredTypes));

        if ($coveredTypes === []) {
            $this->auditStarted(
                auditLogger: $auditLogger,
                tenant: $tenant,
                profile: $profile,
                initiator: $initiator,
                captureMode: $captureMode,
                subjectsTotal: 0,
                effectiveScope: $effectiveScope,
            );

            $this->completeWithCoverageWarning(
                operationRunService: $operationRunService,
                auditLogger: $auditLogger,
                tenant: $tenant,
                profile: $profile,
                initiator: $initiator,
                inventorySyncRun: $inventorySyncRun,
                coverageProof: true,
                effectiveTypes: $effectiveTypes,
                coveredTypes: [],
                uncoveredTypes: $effectiveTypes,
                errorsRecorded: count($effectiveTypes),
                captureMode: $captureMode,
            );

            return;
        }

        $snapshot = BaselineSnapshot::query()
            ->where('workspace_id', (int) $profile->workspace_id)
            ->where('baseline_profile_id', (int) $profile->getKey())
            ->whereKey($snapshotId)
            ->first(['id', 'captured_at']);

        if (! $snapshot instanceof BaselineSnapshot) {
            throw new RuntimeException("BaselineSnapshot #{$snapshotId} not found.");
        }

        $since = $snapshot->captured_at instanceof \DateTimeInterface
            ? CarbonImmutable::instance($snapshot->captured_at)
            : null;

        $baselineResult = $this->loadBaselineItems($snapshotId, $coveredTypes);
        $baselineItems = $baselineResult['items'];
        $baselineGaps = $baselineResult['gaps'];

        $currentResult = $this->loadCurrentInventory($tenant, $coveredTypes, (int) $inventorySyncRun->getKey());
        $currentItems = $currentResult['items'];
        $currentGaps = $currentResult['gaps'];

        $ambiguousKeys = array_values(array_unique(array_filter(array_merge(
            is_array($baselineResult['ambiguous_keys'] ?? null) ? $baselineResult['ambiguous_keys'] : [],
            is_array($currentResult['ambiguous_keys'] ?? null) ? $currentResult['ambiguous_keys'] : [],
        ), 'is_string')));

        foreach ($ambiguousKeys as $ambiguousKey) {
            unset($baselineItems[$ambiguousKey], $currentItems[$ambiguousKey]);
        }

        $subjects = array_values(array_map(
            static fn (array $item): array => [
                'policy_type' => (string) $item['policy_type'],
                'subject_external_id' => (string) $item['subject_external_id'],
            ],
            $currentItems,
        ));

        $subjectsTotal = count($subjects);

        $this->auditStarted(
            auditLogger: $auditLogger,
            tenant: $tenant,
            profile: $profile,
            initiator: $initiator,
            captureMode: $captureMode,
            subjectsTotal: $subjectsTotal,
            effectiveScope: $effectiveScope,
        );

        $phaseStats = [
            'requested' => 0,
            'succeeded' => 0,
            'skipped' => 0,
            'failed' => 0,
            'throttled' => 0,
        ];
        $phaseResult = [];
        $phaseGaps = [];
        $resumeToken = null;

        if ($captureMode === BaselineCaptureMode::FullContent) {
            $budgets = [
                'max_items_per_run' => (int) config('tenantpilot.baselines.full_content_capture.max_items_per_run', 200),
                'max_concurrency' => (int) config('tenantpilot.baselines.full_content_capture.max_concurrency', 5),
                'max_retries' => (int) config('tenantpilot.baselines.full_content_capture.max_retries', 3),
            ];

            $resumeTokenIn = null;

            if (is_array($context['baseline_compare'] ?? null)) {
                $resumeTokenIn = $context['baseline_compare']['resume_token'] ?? null;
            }

            $phaseResult = $contentCapturePhase->capture(
                tenant: $tenant,
                subjects: $subjects,
                purpose: PolicyVersionCapturePurpose::BaselineCompare,
                budgets: $budgets,
                resumeToken: is_string($resumeTokenIn) ? $resumeTokenIn : null,
                operationRunId: (int) $this->operationRun->getKey(),
                baselineProfileId: (int) $profile->getKey(),
                createdBy: $initiator?->email,
            );

            $phaseStats = is_array($phaseResult['stats'] ?? null) ? $phaseResult['stats'] : $phaseStats;
            $phaseGaps = is_array($phaseResult['gaps'] ?? null) ? $phaseResult['gaps'] : [];
            $resumeToken = is_string($phaseResult['resume_token'] ?? null) ? $phaseResult['resume_token'] : null;
        }

        $resolvedCurrentEvidenceByExternalId = $hashResolver->resolveForSubjects(
            tenant: $tenant,
            subjects: $subjects,
            since: $since,
            latestInventorySyncRunId: (int) $inventorySyncRun->getKey(),
        );

        $resolvedCurrentEvidenceByExternalId = array_replace(
            $resolvedCurrentEvidenceByExternalId,
            $this->resolveCapturedCurrentEvidenceByExternalId($phaseResult),
        );

        $resolvedCurrentMetaEvidenceByExternalId = $metaEvidenceProvider->resolve(
            tenant: $tenant,
            subjects: $subjects,
            since: $since,
            latestInventorySyncRunId: (int) $inventorySyncRun->getKey(),
        );

        $resolvedCurrentEvidence = $this->rekeyResolvedEvidenceBySubjectKey(
            currentItems: $currentItems,
            resolvedByExternalId: $resolvedCurrentEvidenceByExternalId,
        );

        $resolvedCurrentMetaEvidence = $this->rekeyResolvedEvidenceBySubjectKey(
            currentItems: $currentItems,
            resolvedByExternalId: $resolvedCurrentMetaEvidenceByExternalId,
        );

        $resolvedEffectiveCurrentEvidence = $this->resolveEffectiveCurrentEvidence(
            baselineItems: $baselineItems,
            currentItems: $currentItems,
            resolvedBestEvidence: $resolvedCurrentEvidence,
            resolvedMetaEvidence: $resolvedCurrentMetaEvidence,
        );

        $baselinePolicyVersionResolver = app(BaselinePolicyVersionResolver::class);
        $driftHasher = app(DriftHasher::class);
        $settingsNormalizer = app(SettingsNormalizer::class);
        $assignmentsNormalizer = app(AssignmentsNormalizer::class);
        $scopeTagsNormalizer = app(ScopeTagsNormalizer::class);

        $computeResult = $this->computeDrift(
            tenant: $tenant,
            baselineProfileId: (int) $profile->getKey(),
            baselineSnapshotId: (int) $snapshot->getKey(),
            compareOperationRunId: (int) $this->operationRun->getKey(),
            inventorySyncRunId: (int) $inventorySyncRun->getKey(),
            baselineItems: $baselineItems,
            currentItems: $currentItems,
            resolvedCurrentEvidence: $resolvedEffectiveCurrentEvidence,
            severityMapping: $this->resolveSeverityMapping($workspace, $settingsResolver),
            baselinePolicyVersionResolver: $baselinePolicyVersionResolver,
            hasher: $driftHasher,
            settingsNormalizer: $settingsNormalizer,
            assignmentsNormalizer: $assignmentsNormalizer,
            scopeTagsNormalizer: $scopeTagsNormalizer,
            contentEvidenceProvider: $contentEvidenceProvider,
        );
        $driftResults = $computeResult['drift'];
        $driftGaps = $computeResult['evidence_gaps'];
        $rbacRoleDefinitionSummary = $computeResult['rbac_role_definitions'];

        $upsertResult = $this->upsertFindings(
            $tenant,
            $profile,
            $scopeKey,
            $driftResults,
        );

        $severityBreakdown = $this->countBySeverity($driftResults);
        $countsByChangeType = $this->countByChangeType($driftResults);

        $gapsByReason = $this->mergeGapCounts($baselineGaps, $currentGaps, $phaseGaps, $driftGaps);
        $gapsCount = array_sum($gapsByReason);

        $summaryCounts = [
            'total' => count($driftResults),
            'processed' => count($driftResults),
            'succeeded' => (int) $upsertResult['processed_count'],
            'failed' => 0,
            'errors_recorded' => count($uncoveredTypes),
            'high' => $severityBreakdown[Finding::SEVERITY_HIGH] ?? 0,
            'medium' => $severityBreakdown[Finding::SEVERITY_MEDIUM] ?? 0,
            'low' => $severityBreakdown[Finding::SEVERITY_LOW] ?? 0,
            'findings_created' => (int) $upsertResult['created_count'],
            'findings_reopened' => (int) $upsertResult['reopened_count'],
            'findings_unchanged' => (int) $upsertResult['unchanged_count'],
        ];

        $warningsRecorded = $uncoveredTypes !== [] || $resumeToken !== null || $gapsByReason !== [];
        $outcome = $warningsRecorded ? OperationRunOutcome::PartiallySucceeded->value : OperationRunOutcome::Succeeded->value;

        $operationRunService->updateRun(
            $this->operationRun,
            status: OperationRunStatus::Completed->value,
            outcome: $outcome,
            summaryCounts: $summaryCounts,
        );

        $resolvedCount = 0;

        if ($baselineAutoCloseService->shouldAutoClose($tenant, $this->operationRun)) {
            $resolvedCount = $baselineAutoCloseService->resolveStaleFindings(
                tenant: $tenant,
                baselineProfileId: (int) $profile->getKey(),
                seenFingerprints: $upsertResult['seen_fingerprints'],
                currentOperationRunId: (int) $this->operationRun->getKey(),
            );

            $summaryCounts['findings_resolved'] = $resolvedCount;

            $operationRunService->updateRun(
                $this->operationRun,
                status: OperationRunStatus::Completed->value,
                outcome: $outcome,
                summaryCounts: $summaryCounts,
            );
        }

        $coverageBreakdown = $this->summarizeCurrentEvidenceCoverage($currentItems, $resolvedEffectiveCurrentEvidence);
        $baselineCoverage = $this->summarizeBaselineEvidenceCoverage($baselineItems);

        $overallFidelity = ($baselineCoverage['baseline_meta'] ?? 0) > 0
            || ($coverageBreakdown['resolved_meta'] ?? 0) > 0
            || ($gapsByReason['missing_current'] ?? 0) > 0
            ? EvidenceProvenance::FidelityMeta
            : EvidenceProvenance::FidelityContent;

        $reasonCode = null;

        if ($subjectsTotal === 0) {
            $reasonCode = BaselineCompareReasonCode::NoSubjectsInScope;
        } elseif (count($driftResults) === 0) {
            $reasonCode = match (true) {
                $uncoveredTypes !== [] => BaselineCompareReasonCode::CoverageUnproven,
                $resumeToken !== null || $gapsCount > 0 => BaselineCompareReasonCode::EvidenceCaptureIncomplete,
                default => BaselineCompareReasonCode::NoDriftDetected,
            };
        }

        $updatedContext = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $updatedContext['baseline_compare'] = array_merge(
            is_array($updatedContext['baseline_compare'] ?? null) ? $updatedContext['baseline_compare'] : [],
            [
                'inventory_sync_run_id' => (int) $inventorySyncRun->getKey(),
                'since' => $since?->toIso8601String(),
                'subjects_total' => $subjectsTotal,
                'evidence_capture' => $phaseStats,
                'evidence_gaps' => [
                    'count' => $gapsCount,
                    'by_reason' => $gapsByReason,
                    ...$gapsByReason,
                ],
                'resume_token' => $resumeToken,
                'coverage' => [
                    'effective_types' => $effectiveTypes,
                    'covered_types' => $coveredTypes,
                    'uncovered_types' => $uncoveredTypes,
                    'proof' => true,
                    ...$coverageBreakdown,
                    ...$baselineCoverage,
                ],
                'rbac_role_definitions' => $rbacRoleDefinitionSummary,
                'fidelity' => $overallFidelity,
                'reason_code' => $reasonCode?->value,
            ],
        );
        $updatedContext['findings'] = array_merge(
            is_array($updatedContext['findings'] ?? null) ? $updatedContext['findings'] : [],
            [
                'counts_by_change_type' => $countsByChangeType,
            ],
        );
        $updatedContext['result'] = [
            'findings_total' => count($driftResults),
            'findings_upserted' => (int) $upsertResult['processed_count'],
            'findings_resolved' => $resolvedCount,
            'severity_breakdown' => $severityBreakdown,
        ];
        $this->operationRun->update(['context' => $updatedContext]);

        $this->auditCompleted(
            auditLogger: $auditLogger,
            tenant: $tenant,
            profile: $profile,
            initiator: $initiator,
            captureMode: $captureMode,
            subjectsTotal: $subjectsTotal,
            evidenceCaptureStats: $phaseStats,
            gaps: [
                'count' => $gapsCount,
                'by_reason' => $gapsByReason,
            ],
            summaryCounts: $summaryCounts,
        );
    }

    /**
     * Baseline hashes depend on which evidence fidelity was available at capture time (content vs meta).
     *
     * Current state evidence is therefore selected to be comparable to the baseline hash:
     * - If baseline evidence fidelity is meta: force meta evidence for current (inventory meta contract).
     * - If baseline evidence fidelity is content: require current content evidence (since-rule); otherwise treat as a gap.
     *
     * @param  array<string, array{subject_type: string, subject_external_id: string, policy_type: string, baseline_hash: string, meta_jsonb: array<string, mixed>}>  $baselineItems
     * @param  array<string, array{subject_external_id: string, policy_type: string, meta_jsonb: array<string, mixed>}>  $currentItems
     * @param  array<string, ResolvedEvidence|null>  $resolvedBestEvidence
     * @param  array<string, ResolvedEvidence>  $resolvedMetaEvidence
     * @return array<string, ResolvedEvidence|null>
     */
    private function resolveEffectiveCurrentEvidence(
        array $baselineItems,
        array $currentItems,
        array $resolvedBestEvidence,
        array $resolvedMetaEvidence,
    ): array {
        /**
         * @var array<string, string>
         */
        $baselineFidelityByKey = [];

        foreach ($baselineItems as $key => $baselineItem) {
            $provenance = $this->baselineProvenanceFromMetaJsonb($baselineItem['meta_jsonb'] ?? []);
            $baselineFidelityByKey[$key] = (string) ($provenance['fidelity'] ?? EvidenceProvenance::FidelityMeta);
        }

        $effective = [];

        foreach ($currentItems as $key => $currentItem) {
            if (array_key_exists($key, $baselineItems)) {
                $baselineFidelity = $baselineFidelityByKey[$key] ?? EvidenceProvenance::FidelityMeta;

                if ($baselineFidelity === EvidenceProvenance::FidelityMeta) {
                    $effective[$key] = $resolvedMetaEvidence[$key] ?? null;

                    continue;
                }

                $best = $resolvedBestEvidence[$key] ?? null;

                if ($best instanceof ResolvedEvidence && $best->fidelity === EvidenceProvenance::FidelityContent) {
                    $effective[$key] = $best;
                } else {
                    $effective[$key] = null;
                }

                continue;
            }

            $effective[$key] = $resolvedBestEvidence[$key] ?? null;
        }

        return $effective;
    }

    /**
     * Rekey resolved evidence from "policy_type|external_id" to the current items key ("policy_type|subject_key").
     *
     * @param  array<string, array{subject_external_id: string, policy_type: string}>  $currentItems
     * @param  array<string, ResolvedEvidence|null>  $resolvedByExternalId
     * @return array<string, ResolvedEvidence|null>
     */
    private function rekeyResolvedEvidenceBySubjectKey(array $currentItems, array $resolvedByExternalId): array
    {
        $rekeyed = [];

        foreach ($currentItems as $key => $currentItem) {
            $policyType = (string) ($currentItem['policy_type'] ?? '');
            $externalId = (string) ($currentItem['subject_external_id'] ?? '');

            if ($policyType === '' || $externalId === '') {
                $rekeyed[$key] = null;

                continue;
            }

            $resolvedKey = $policyType.'|'.$externalId;
            $rekeyed[$key] = $resolvedByExternalId[$resolvedKey] ?? null;
        }

        return $rekeyed;
    }

    /**
     * @param  array{
     *   captured_versions?: array<string, array{
     *     policy_type: string,
     *     subject_external_id: string,
     *     version: PolicyVersion,
     *     observed_at: string,
     *     observed_operation_run_id: ?int
     *   }>
     * }  $phaseResult
     * @return array<string, ResolvedEvidence>
     */
    private function resolveCapturedCurrentEvidenceByExternalId(array $phaseResult): array
    {
        $capturedVersions = is_array($phaseResult['captured_versions'] ?? null)
            ? $phaseResult['captured_versions']
            : [];

        if ($capturedVersions === []) {
            return [];
        }

        $contentEvidenceProvider = app(ContentEvidenceProvider::class);
        $resolved = [];

        foreach ($capturedVersions as $key => $capturedVersion) {
            $version = $capturedVersion['version'] ?? null;
            $subjectExternalId = trim((string) ($capturedVersion['subject_external_id'] ?? ''));
            $observedAt = $capturedVersion['observed_at'] ?? null;
            $observedAt = is_string($observedAt) && $observedAt !== ''
                ? CarbonImmutable::parse($observedAt)
                : null;
            $observedOperationRunId = $capturedVersion['observed_operation_run_id'] ?? null;
            $observedOperationRunId = is_numeric($observedOperationRunId) ? (int) $observedOperationRunId : null;

            if (! $version instanceof PolicyVersion || $subjectExternalId === '' || ! is_string($key) || $key === '') {
                continue;
            }

            $resolved[$key] = $contentEvidenceProvider->fromPolicyVersion(
                version: $version,
                subjectExternalId: $subjectExternalId,
                observedAt: $observedAt,
                observedOperationRunId: $observedOperationRunId,
            );
        }

        return $resolved;
    }

    private function completeWithCoverageWarning(
        OperationRunService $operationRunService,
        AuditLogger $auditLogger,
        Tenant $tenant,
        BaselineProfile $profile,
        ?User $initiator,
        ?OperationRun $inventorySyncRun,
        bool $coverageProof,
        array $effectiveTypes,
        array $coveredTypes,
        array $uncoveredTypes,
        int $errorsRecorded,
        BaselineCaptureMode $captureMode,
        BaselineCompareReasonCode $reasonCode = BaselineCompareReasonCode::CoverageUnproven,
        ?array $evidenceGapsByReason = null,
    ): void {
        $summaryCounts = [
            'total' => 0,
            'processed' => 0,
            'succeeded' => 0,
            'failed' => 0,
            'errors_recorded' => max(1, $errorsRecorded),
            'high' => 0,
            'medium' => 0,
            'low' => 0,
            'findings_created' => 0,
            'findings_reopened' => 0,
            'findings_unchanged' => 0,
        ];

        $operationRunService->updateRun(
            $this->operationRun,
            status: OperationRunStatus::Completed->value,
            outcome: OperationRunOutcome::PartiallySucceeded->value,
            summaryCounts: $summaryCounts,
        );

        $updatedContext = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $evidenceCapture = [
            'requested' => 0,
            'succeeded' => 0,
            'skipped' => 0,
            'failed' => 0,
            'throttled' => 0,
        ];

        $evidenceGapsByReason ??= [
            BaselineCompareReasonCode::CoverageUnproven->value => max(1, $errorsRecorded),
        ];

        $updatedContext['baseline_compare'] = array_merge(
            is_array($updatedContext['baseline_compare'] ?? null) ? $updatedContext['baseline_compare'] : [],
            [
                'inventory_sync_run_id' => $inventorySyncRun instanceof OperationRun ? (int) $inventorySyncRun->getKey() : null,
                'subjects_total' => 0,
                'evidence_capture' => $evidenceCapture,
                'evidence_gaps' => [
                    'count' => array_sum($evidenceGapsByReason),
                    'by_reason' => $evidenceGapsByReason,
                    ...$evidenceGapsByReason,
                ],
                'resume_token' => null,
                'reason_code' => $reasonCode->value,
                'coverage' => [
                    'effective_types' => array_values($effectiveTypes),
                    'covered_types' => array_values($coveredTypes),
                    'uncovered_types' => array_values($uncoveredTypes),
                    'proof' => $coverageProof,
                    'subjects_total' => 0,
                    'resolved_total' => 0,
                    'resolved_content' => 0,
                    'resolved_meta' => 0,
                    'policy_types_content' => [],
                    'policy_types_meta_only' => [],
                ],
                'rbac_role_definitions' => $this->emptyRbacRoleDefinitionSummary(),
                'fidelity' => 'meta',
            ],
        );
        $updatedContext['findings'] = array_merge(
            is_array($updatedContext['findings'] ?? null) ? $updatedContext['findings'] : [],
            [
                'counts_by_change_type' => [],
            ],
        );
        $updatedContext['result'] = [
            'findings_total' => 0,
            'findings_upserted' => 0,
            'findings_resolved' => 0,
            'severity_breakdown' => [],
        ];

        $this->operationRun->update(['context' => $updatedContext]);

        $this->auditCompleted(
            auditLogger: $auditLogger,
            tenant: $tenant,
            profile: $profile,
            initiator: $initiator,
            captureMode: $captureMode,
            subjectsTotal: 0,
            evidenceCaptureStats: $evidenceCapture,
            gaps: [
                'count' => array_sum($evidenceGapsByReason),
                'by_reason' => $evidenceGapsByReason,
            ],
            summaryCounts: $summaryCounts,
        );
    }

    /**
     * Load baseline snapshot items keyed by "policy_type|subject_key".
     *
     * @return array{
     *   items: array<string, array{subject_type: string, subject_external_id: string, subject_key: string, policy_type: string, baseline_hash: string, meta_jsonb: array<string, mixed>}>,
     *   gaps: array<string, int>,
     *   ambiguous_keys: list<string>
     * }
     */
    private function loadBaselineItems(int $snapshotId, array $policyTypes): array
    {
        $items = [];
        $gaps = [];

        /**
         * @var array<string, true>
         */
        $ambiguousKeys = [];

        if ($policyTypes === []) {
            return [
                'items' => $items,
                'gaps' => $gaps,
                'ambiguous_keys' => [],
            ];
        }

        $query = BaselineSnapshotItem::query()
            ->where('baseline_snapshot_id', $snapshotId);

        $query->whereIn('policy_type', $policyTypes);

        $query
            ->orderBy('id')
            ->chunk(500, function ($snapshotItems) use (&$items, &$gaps, &$ambiguousKeys): void {
                foreach ($snapshotItems as $item) {
                    $metaJsonb = is_array($item->meta_jsonb) ? $item->meta_jsonb : [];
                    $displayName = $metaJsonb['display_name'] ?? ($metaJsonb['displayName'] ?? null);
                    $displayName = is_string($displayName) ? $displayName : null;

                    $subjectKey = $this->normalizeSubjectKey(
                        policyType: (string) $item->policy_type,
                        storedSubjectKey: is_string($item->subject_key) ? $item->subject_key : null,
                        displayName: $displayName,
                    );

                    if ($subjectKey === '') {
                        $gaps['missing_subject_key_baseline'] = ($gaps['missing_subject_key_baseline'] ?? 0) + 1;

                        continue;
                    }

                    $key = $item->policy_type.'|'.$subjectKey;

                    if (array_key_exists($key, $ambiguousKeys)) {
                        continue;
                    }

                    if (array_key_exists($key, $items)) {
                        $ambiguousKeys[$key] = true;
                        unset($items[$key]);

                        $gaps['ambiguous_match'] = ($gaps['ambiguous_match'] ?? 0) + 1;

                        continue;
                    }

                    $items[$key] = [
                        'subject_type' => (string) $item->subject_type,
                        'subject_external_id' => (string) $item->subject_external_id,
                        'subject_key' => $subjectKey,
                        'policy_type' => (string) $item->policy_type,
                        'baseline_hash' => (string) $item->baseline_hash,
                        'meta_jsonb' => $metaJsonb,
                    ];
                }
            });

        ksort($gaps);

        return [
            'items' => $items,
            'gaps' => $gaps,
            'ambiguous_keys' => array_values(array_keys($ambiguousKeys)),
        ];
    }

    /**
     * Load current inventory items keyed by "policy_type|subject_key".
     *
     * @return array{
     *   items: array<string, array{subject_external_id: string, subject_key: string, policy_type: string, meta_jsonb: array<string, mixed>}>,
     *   gaps: array<string, int>,
     *   ambiguous_keys: list<string>
     * }
     */
    private function loadCurrentInventory(
        Tenant $tenant,
        array $policyTypes,
        ?int $latestInventorySyncRunId = null,
    ): array {
        $query = InventoryItem::query()
            ->where('tenant_id', $tenant->getKey());

        if (is_int($latestInventorySyncRunId) && $latestInventorySyncRunId > 0) {
            $query->where('last_seen_operation_run_id', $latestInventorySyncRunId);
        }

        if ($policyTypes === []) {
            return [
                'items' => [],
                'gaps' => [],
                'ambiguous_keys' => [],
            ];
        }

        $query->whereIn('policy_type', $policyTypes);

        $items = [];
        $gaps = [];

        /**
         * @var array<string, true>
         */
        $ambiguousKeys = [];

        $query->orderBy('policy_type')
            ->orderBy('external_id')
            ->chunk(500, function ($inventoryItems) use (&$items, &$gaps, &$ambiguousKeys): void {
                foreach ($inventoryItems as $inventoryItem) {
                    $metaJsonb = is_array($inventoryItem->meta_jsonb) ? $inventoryItem->meta_jsonb : [];
                    $subjectKey = $this->normalizeSubjectKey(
                        policyType: (string) $inventoryItem->policy_type,
                        displayName: is_string($inventoryItem->display_name) ? $inventoryItem->display_name : null,
                        subjectExternalId: is_string($inventoryItem->external_id) ? $inventoryItem->external_id : null,
                    );

                    if ($subjectKey === '') {
                        $gaps['missing_subject_key_current'] = ($gaps['missing_subject_key_current'] ?? 0) + 1;

                        continue;
                    }

                    $key = $inventoryItem->policy_type.'|'.$subjectKey;

                    if (array_key_exists($key, $ambiguousKeys)) {
                        continue;
                    }

                    if (array_key_exists($key, $items)) {
                        $ambiguousKeys[$key] = true;
                        unset($items[$key]);

                        $gaps['ambiguous_match'] = ($gaps['ambiguous_match'] ?? 0) + 1;

                        continue;
                    }

                    $items[$key] = [
                        'subject_external_id' => (string) $inventoryItem->external_id,
                        'subject_key' => $subjectKey,
                        'policy_type' => (string) $inventoryItem->policy_type,
                        'meta_jsonb' => [
                            'display_name' => $inventoryItem->display_name,
                            'category' => $inventoryItem->category,
                            'platform' => $inventoryItem->platform,
                            'is_built_in' => $metaJsonb['is_built_in'] ?? null,
                            'role_permission_count' => $metaJsonb['role_permission_count'] ?? null,
                        ],
                    ];
                }
            });

        ksort($gaps);

        return [
            'items' => $items,
            'gaps' => $gaps,
            'ambiguous_keys' => array_values(array_keys($ambiguousKeys)),
        ];
    }

    private function resolveLatestInventorySyncRun(Tenant $tenant): ?OperationRun
    {
        $run = OperationRun::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->where('type', OperationRunType::InventorySync->value)
            ->where('status', OperationRunStatus::Completed->value)
            ->orderByDesc('completed_at')
            ->orderByDesc('id')
            ->first();

        return $run instanceof OperationRun ? $run : null;
    }

    /**
     * Compare baseline items vs current inventory and produce drift results.
     *
     * @param  array<string, array{subject_type: string, subject_external_id: string, subject_key: string, policy_type: string, baseline_hash: string, meta_jsonb: array<string, mixed>}>  $baselineItems
     * @param  array<string, array{subject_external_id: string, subject_key: string, policy_type: string, meta_jsonb: array<string, mixed>}>  $currentItems
     * @param  array<string, ResolvedEvidence|null>  $resolvedCurrentEvidence
     * @param  array<string, string>  $severityMapping
     * @return array{
     *   drift: array<int, array{change_type: string, severity: string, evidence_fidelity: string, subject_type: string, subject_external_id: string, subject_key: string, policy_type: string, baseline_hash: string, current_hash: string, evidence: array<string, mixed>}>,
     *   evidence_gaps: array<string, int>,
     *   rbac_role_definitions: array{total_compared: int, unchanged: int, modified: int, missing: int, unexpected: int}
     * }
     */
    private function computeDrift(
        Tenant $tenant,
        int $baselineProfileId,
        int $baselineSnapshotId,
        int $compareOperationRunId,
        int $inventorySyncRunId,
        array $baselineItems,
        array $currentItems,
        array $resolvedCurrentEvidence,
        array $severityMapping,
        BaselinePolicyVersionResolver $baselinePolicyVersionResolver,
        DriftHasher $hasher,
        SettingsNormalizer $settingsNormalizer,
        AssignmentsNormalizer $assignmentsNormalizer,
        ScopeTagsNormalizer $scopeTagsNormalizer,
        ContentEvidenceProvider $contentEvidenceProvider,
    ): array {
        $drift = [];
        $evidenceGaps = [];
        $rbacRoleDefinitionSummary = $this->emptyRbacRoleDefinitionSummary();
        $roleDefinitionNormalizer = app(IntuneRoleDefinitionNormalizer::class);

        $baselinePlaceholderProvenance = EvidenceProvenance::build(
            fidelity: EvidenceProvenance::FidelityMeta,
            source: EvidenceProvenance::SourceInventory,
            observedAt: null,
            observedOperationRunId: null,
        );

        $currentMissingProvenance = EvidenceProvenance::build(
            fidelity: EvidenceProvenance::FidelityMeta,
            source: EvidenceProvenance::SourceInventory,
            observedAt: null,
            observedOperationRunId: $inventorySyncRunId,
        );

        foreach ($baselineItems as $key => $baselineItem) {
            $currentItem = $currentItems[$key] ?? null;

            $policyType = (string) ($baselineItem['policy_type'] ?? '');
            $subjectKey = (string) ($baselineItem['subject_key'] ?? '');
            $isRbacRoleDefinition = $policyType === 'intuneRoleDefinition';

            $baselineProvenance = $this->baselineProvenanceFromMetaJsonb($baselineItem['meta_jsonb'] ?? []);
            $baselinePolicyVersionId = $this->resolveBaselinePolicyVersionId(
                tenant: $tenant,
                baselineItem: $baselineItem,
                baselineProvenance: $baselineProvenance,
                baselinePolicyVersionResolver: $baselinePolicyVersionResolver,
            );
            $baselineComparableHash = $this->effectiveBaselineHash(
                tenant: $tenant,
                baselineItem: $baselineItem,
                baselinePolicyVersionId: $baselinePolicyVersionId,
                contentEvidenceProvider: $contentEvidenceProvider,
            );

            if (! is_array($currentItem)) {
                if ($isRbacRoleDefinition && $baselinePolicyVersionId === null) {
                    $evidenceGaps['missing_role_definition_baseline_version_reference'] = ($evidenceGaps['missing_role_definition_baseline_version_reference'] ?? 0) + 1;

                    continue;
                }

                $displayName = $baselineItem['meta_jsonb']['display_name'] ?? null;
                $displayName = is_string($displayName) ? (string) $displayName : null;

                $evidence = $this->buildDriftEvidenceContract(
                    changeType: 'missing_policy',
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    displayName: $displayName,
                    baselineHash: $baselineComparableHash,
                    currentHash: null,
                    baselineProvenance: $baselineProvenance,
                    currentProvenance: $currentMissingProvenance,
                    baselinePolicyVersionId: $baselinePolicyVersionId,
                    currentPolicyVersionId: null,
                    summaryKind: 'policy_snapshot',
                    baselineProfileId: $baselineProfileId,
                    baselineSnapshotId: $baselineSnapshotId,
                    compareOperationRunId: $compareOperationRunId,
                    inventorySyncRunId: $inventorySyncRunId,
                );

                if ($isRbacRoleDefinition) {
                    $evidence['summary']['kind'] = 'rbac_role_definition';
                    $evidence['rbac_role_definition'] = $this->buildRoleDefinitionEvidencePayload(
                        tenant: $tenant,
                        baselinePolicyVersionId: $baselinePolicyVersionId,
                        currentPolicyVersionId: null,
                        baselineMeta: is_array($baselineItem['meta_jsonb'] ?? null) ? $baselineItem['meta_jsonb'] : [],
                        currentMeta: [],
                        diffKind: 'missing',
                    );
                }

                if ($isRbacRoleDefinition) {
                    $rbacRoleDefinitionSummary['missing']++;
                    $rbacRoleDefinitionSummary['total_compared']++;
                }

                $drift[] = [
                    'change_type' => 'missing_policy',
                    'severity' => $isRbacRoleDefinition
                        ? Finding::SEVERITY_HIGH
                        : $this->severityForChangeType($severityMapping, 'missing_policy'),
                    'subject_type' => $baselineItem['subject_type'],
                    'subject_external_id' => $baselineItem['subject_external_id'],
                    'subject_key' => $subjectKey,
                    'policy_type' => $policyType,
                    'evidence_fidelity' => (string) ($evidence['fidelity'] ?? EvidenceProvenance::FidelityMeta),
                    'baseline_hash' => $baselineComparableHash,
                    'current_hash' => '',
                    'evidence' => $evidence,
                ];

                continue;
            }

            $currentEvidence = $resolvedCurrentEvidence[$key] ?? null;

            if (! $currentEvidence instanceof ResolvedEvidence) {
                $evidenceGaps['missing_current'] = ($evidenceGaps['missing_current'] ?? 0) + 1;

                continue;
            }

            $currentPolicyVersionId = $this->currentPolicyVersionIdFromEvidence($currentEvidence);

            if ($baselineComparableHash !== $currentEvidence->hash) {
                $displayName = $currentItem['meta_jsonb']['display_name']
                    ?? ($baselineItem['meta_jsonb']['display_name'] ?? null);

                $displayName = is_string($displayName) ? (string) $displayName : null;
                $roleDefinitionDiff = null;

                if ($isRbacRoleDefinition) {
                    if ($baselinePolicyVersionId === null) {
                        $evidenceGaps['missing_role_definition_baseline_version_reference'] = ($evidenceGaps['missing_role_definition_baseline_version_reference'] ?? 0) + 1;

                        continue;
                    }

                    if ($currentPolicyVersionId === null) {
                        $evidenceGaps['missing_role_definition_current_version_reference'] = ($evidenceGaps['missing_role_definition_current_version_reference'] ?? 0) + 1;

                        continue;
                    }

                    $roleDefinitionDiff = $this->resolveRoleDefinitionDiff(
                        tenant: $tenant,
                        baselinePolicyVersionId: $baselinePolicyVersionId,
                        currentPolicyVersionId: $currentPolicyVersionId,
                        normalizer: $roleDefinitionNormalizer,
                    );

                    if ($roleDefinitionDiff === null) {
                        $evidenceGaps['missing_role_definition_compare_surface'] = ($evidenceGaps['missing_role_definition_compare_surface'] ?? 0) + 1;

                        continue;
                    }
                }

                $summaryKind = $isRbacRoleDefinition
                    ? 'rbac_role_definition'
                    : $this->selectSummaryKind(
                        tenant: $tenant,
                        policyType: $policyType,
                        baselinePolicyVersionId: $baselinePolicyVersionId,
                        currentPolicyVersionId: $currentPolicyVersionId,
                        hasher: $hasher,
                        settingsNormalizer: $settingsNormalizer,
                        assignmentsNormalizer: $assignmentsNormalizer,
                        scopeTagsNormalizer: $scopeTagsNormalizer,
                    );

                $evidence = $this->buildDriftEvidenceContract(
                    changeType: 'different_version',
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    displayName: $displayName,
                    baselineHash: $baselineComparableHash,
                    currentHash: (string) $currentEvidence->hash,
                    baselineProvenance: $baselineProvenance,
                    currentProvenance: $currentEvidence->tenantProvenance(),
                    baselinePolicyVersionId: $baselinePolicyVersionId,
                    currentPolicyVersionId: $currentPolicyVersionId,
                    summaryKind: $summaryKind,
                    baselineProfileId: $baselineProfileId,
                    baselineSnapshotId: $baselineSnapshotId,
                    compareOperationRunId: $compareOperationRunId,
                    inventorySyncRunId: $inventorySyncRunId,
                );

                if ($isRbacRoleDefinition && is_array($roleDefinitionDiff)) {
                    $evidence['rbac_role_definition'] = $this->buildRoleDefinitionEvidencePayload(
                        tenant: $tenant,
                        baselinePolicyVersionId: $baselinePolicyVersionId,
                        currentPolicyVersionId: $currentPolicyVersionId,
                        baselineMeta: is_array($baselineItem['meta_jsonb'] ?? null) ? $baselineItem['meta_jsonb'] : [],
                        currentMeta: is_array($currentEvidence->meta ?? null) ? $currentEvidence->meta : (is_array($currentItem['meta_jsonb'] ?? null) ? $currentItem['meta_jsonb'] : []),
                        diffKind: (string) $roleDefinitionDiff['diff_kind'],
                        roleDefinitionDiff: $roleDefinitionDiff,
                    );
                    $rbacRoleDefinitionSummary['modified']++;
                    $rbacRoleDefinitionSummary['total_compared']++;
                }

                $drift[] = [
                    'change_type' => 'different_version',
                    'severity' => $isRbacRoleDefinition
                        ? $this->severityForRoleDefinitionDiff($roleDefinitionDiff)
                        : $this->severityForChangeType($severityMapping, 'different_version'),
                    'subject_type' => $baselineItem['subject_type'],
                    'subject_external_id' => $currentItem['subject_external_id'],
                    'subject_key' => $subjectKey,
                    'policy_type' => $policyType,
                    'evidence_fidelity' => (string) ($evidence['fidelity'] ?? EvidenceProvenance::FidelityMeta),
                    'baseline_hash' => $baselineComparableHash,
                    'current_hash' => $currentEvidence->hash,
                    'evidence' => $evidence,
                ];

                continue;
            }

            if ($isRbacRoleDefinition) {
                $rbacRoleDefinitionSummary['unchanged']++;
                $rbacRoleDefinitionSummary['total_compared']++;
            }
        }

        foreach ($currentItems as $key => $currentItem) {
            if (! array_key_exists($key, $baselineItems)) {
                $currentEvidence = $resolvedCurrentEvidence[$key] ?? null;

                if (! $currentEvidence instanceof ResolvedEvidence) {
                    $evidenceGaps['missing_current'] = ($evidenceGaps['missing_current'] ?? 0) + 1;

                    continue;
                }

                $policyType = (string) ($currentItem['policy_type'] ?? '');
                $subjectKey = (string) ($currentItem['subject_key'] ?? '');
                $isRbacRoleDefinition = $policyType === 'intuneRoleDefinition';

                $displayName = $currentItem['meta_jsonb']['display_name'] ?? null;
                $displayName = is_string($displayName) ? (string) $displayName : null;

                $currentPolicyVersionId = $this->currentPolicyVersionIdFromEvidence($currentEvidence);

                if ($isRbacRoleDefinition && $currentPolicyVersionId === null) {
                    $evidenceGaps['missing_role_definition_current_version_reference'] = ($evidenceGaps['missing_role_definition_current_version_reference'] ?? 0) + 1;

                    continue;
                }

                $evidence = $this->buildDriftEvidenceContract(
                    changeType: 'unexpected_policy',
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    displayName: $displayName,
                    baselineHash: null,
                    currentHash: (string) $currentEvidence->hash,
                    baselineProvenance: $baselinePlaceholderProvenance,
                    currentProvenance: $currentEvidence->tenantProvenance(),
                    baselinePolicyVersionId: null,
                    currentPolicyVersionId: $currentPolicyVersionId,
                    summaryKind: 'policy_snapshot',
                    baselineProfileId: $baselineProfileId,
                    baselineSnapshotId: $baselineSnapshotId,
                    compareOperationRunId: $compareOperationRunId,
                    inventorySyncRunId: $inventorySyncRunId,
                );

                if ($isRbacRoleDefinition) {
                    $evidence['summary']['kind'] = 'rbac_role_definition';
                    $evidence['rbac_role_definition'] = $this->buildRoleDefinitionEvidencePayload(
                        tenant: $tenant,
                        baselinePolicyVersionId: null,
                        currentPolicyVersionId: $currentPolicyVersionId,
                        baselineMeta: [],
                        currentMeta: is_array($currentEvidence->meta ?? null) ? $currentEvidence->meta : (is_array($currentItem['meta_jsonb'] ?? null) ? $currentItem['meta_jsonb'] : []),
                        diffKind: 'unexpected',
                    );
                }

                if ($isRbacRoleDefinition) {
                    $rbacRoleDefinitionSummary['unexpected']++;
                    $rbacRoleDefinitionSummary['total_compared']++;
                }

                $drift[] = [
                    'change_type' => 'unexpected_policy',
                    'severity' => $isRbacRoleDefinition
                        ? Finding::SEVERITY_MEDIUM
                        : $this->severityForChangeType($severityMapping, 'unexpected_policy'),
                    'subject_type' => 'policy',
                    'subject_external_id' => $currentItem['subject_external_id'],
                    'subject_key' => $subjectKey,
                    'policy_type' => $policyType,
                    'evidence_fidelity' => (string) ($evidence['fidelity'] ?? EvidenceProvenance::FidelityMeta),
                    'baseline_hash' => '',
                    'current_hash' => $currentEvidence->hash,
                    'evidence' => $evidence,
                ];
            }
        }

        return [
            'drift' => $drift,
            'evidence_gaps' => $evidenceGaps,
            'rbac_role_definitions' => $rbacRoleDefinitionSummary,
        ];
    }

    /**
     * @param  array{subject_external_id: string, baseline_hash: string}  $baselineItem
     */
    private function effectiveBaselineHash(
        Tenant $tenant,
        array $baselineItem,
        ?int $baselinePolicyVersionId,
        ContentEvidenceProvider $contentEvidenceProvider,
    ): string {
        $storedHash = (string) ($baselineItem['baseline_hash'] ?? '');

        if ($baselinePolicyVersionId === null) {
            return $storedHash;
        }

        if (array_key_exists($baselinePolicyVersionId, $this->baselineContentHashCache)) {
            return $this->baselineContentHashCache[$baselinePolicyVersionId];
        }

        $baselineVersion = PolicyVersion::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->find($baselinePolicyVersionId);

        if (! $baselineVersion instanceof PolicyVersion) {
            return $storedHash;
        }

        $hash = $contentEvidenceProvider->fromPolicyVersion(
            version: $baselineVersion,
            subjectExternalId: (string) ($baselineItem['subject_external_id'] ?? ''),
        )->hash;

        $this->baselineContentHashCache[$baselinePolicyVersionId] = $hash;

        return $hash;
    }

    private function resolveBaselinePolicyVersionId(
        Tenant $tenant,
        array $baselineItem,
        array $baselineProvenance,
        BaselinePolicyVersionResolver $baselinePolicyVersionResolver,
    ): ?int {
        $metaJsonb = is_array($baselineItem['meta_jsonb'] ?? null) ? $baselineItem['meta_jsonb'] : [];
        $versionReferenceId = data_get($metaJsonb, 'version_reference.policy_version_id');

        if (is_numeric($versionReferenceId)) {
            return (int) $versionReferenceId;
        }

        $baselineFidelity = (string) ($baselineProvenance['fidelity'] ?? EvidenceProvenance::FidelityMeta);
        $baselineSource = (string) ($baselineProvenance['source'] ?? EvidenceProvenance::SourceInventory);

        if ($baselineFidelity !== EvidenceProvenance::FidelityContent || $baselineSource !== EvidenceProvenance::SourcePolicyVersion) {
            return null;
        }

        $observedAt = $baselineProvenance['observed_at'] ?? null;
        $observedAt = is_string($observedAt) ? trim($observedAt) : null;

        if (! is_string($observedAt) || $observedAt === '') {
            return null;
        }

        return $baselinePolicyVersionResolver->resolve(
            tenant: $tenant,
            policyType: (string) ($baselineItem['policy_type'] ?? ''),
            subjectKey: (string) ($baselineItem['subject_key'] ?? ''),
            observedAt: $observedAt,
        );
    }

    private function currentPolicyVersionIdFromEvidence(ResolvedEvidence $evidence): ?int
    {
        $policyVersionId = $evidence->meta['policy_version_id'] ?? null;

        return is_numeric($policyVersionId) ? (int) $policyVersionId : null;
    }

    private function selectSummaryKind(
        Tenant $tenant,
        string $policyType,
        ?int $baselinePolicyVersionId,
        ?int $currentPolicyVersionId,
        DriftHasher $hasher,
        SettingsNormalizer $settingsNormalizer,
        AssignmentsNormalizer $assignmentsNormalizer,
        ScopeTagsNormalizer $scopeTagsNormalizer,
    ): string {
        if ($baselinePolicyVersionId === null || $currentPolicyVersionId === null) {
            return 'policy_snapshot';
        }

        $baselineVersion = PolicyVersion::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->find($baselinePolicyVersionId);

        $currentVersion = PolicyVersion::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->find($currentPolicyVersionId);

        if (! $baselineVersion instanceof PolicyVersion || ! $currentVersion instanceof PolicyVersion) {
            return 'policy_snapshot';
        }

        $platform = is_string($baselineVersion->platform ?? null)
            ? (string) $baselineVersion->platform
            : (is_string($currentVersion->platform ?? null) ? (string) $currentVersion->platform : null);

        $baselineSnapshot = is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [];
        $currentSnapshot = is_array($currentVersion->snapshot) ? $currentVersion->snapshot : [];

        $baselineNormalized = $settingsNormalizer->normalizeForDiff(
            snapshot: $baselineSnapshot,
            policyType: $policyType,
            platform: $platform,
        );
        $currentNormalized = $settingsNormalizer->normalizeForDiff(
            snapshot: $currentSnapshot,
            policyType: $policyType,
            platform: $platform,
        );

        $baselineSnapshotHash = $hasher->hashNormalized([
            'settings' => $baselineNormalized,
            'secret_fingerprints' => $this->fingerprintBucket($baselineVersion, 'snapshot'),
        ]);
        $currentSnapshotHash = $hasher->hashNormalized([
            'settings' => $currentNormalized,
            'secret_fingerprints' => $this->fingerprintBucket($currentVersion, 'snapshot'),
        ]);

        if ($baselineSnapshotHash !== $currentSnapshotHash) {
            return 'policy_snapshot';
        }

        $baselineAssignments = is_array($baselineVersion->assignments) ? $baselineVersion->assignments : [];
        $currentAssignments = is_array($currentVersion->assignments) ? $currentVersion->assignments : [];

        $baselineAssignmentsHash = $hasher->hashNormalized([
            'assignments' => $assignmentsNormalizer->normalizeForDiff($baselineAssignments),
            'secret_fingerprints' => $this->fingerprintBucket($baselineVersion, 'assignments'),
        ]);
        $currentAssignmentsHash = $hasher->hashNormalized([
            'assignments' => $assignmentsNormalizer->normalizeForDiff($currentAssignments),
            'secret_fingerprints' => $this->fingerprintBucket($currentVersion, 'assignments'),
        ]);

        if ($baselineAssignmentsHash !== $currentAssignmentsHash) {
            return 'policy_assignments';
        }

        $baselineScopeTagIds = $scopeTagsNormalizer->normalizeIdsForHash($baselineVersion->scope_tags);
        $currentScopeTagIds = $scopeTagsNormalizer->normalizeIdsForHash($currentVersion->scope_tags);

        if ($baselineScopeTagIds === null || $currentScopeTagIds === null) {
            return 'policy_snapshot';
        }

        $baselineScopeTagsHash = $hasher->hashNormalized([
            'scope_tag_ids' => $baselineScopeTagIds,
            'secret_fingerprints' => $this->fingerprintBucket($baselineVersion, 'scope_tags'),
        ]);
        $currentScopeTagsHash = $hasher->hashNormalized([
            'scope_tag_ids' => $currentScopeTagIds,
            'secret_fingerprints' => $this->fingerprintBucket($currentVersion, 'scope_tags'),
        ]);

        if ($baselineScopeTagsHash !== $currentScopeTagsHash) {
            return 'policy_scope_tags';
        }

        return 'policy_snapshot';
    }

    /**
     * @return array<string, string>
     */
    private function fingerprintBucket(PolicyVersion $version, string $bucket): array
    {
        $secretFingerprints = is_array($version->secret_fingerprints) ? $version->secret_fingerprints : [];
        $bucketFingerprints = $secretFingerprints[$bucket] ?? [];

        return is_array($bucketFingerprints) ? $bucketFingerprints : [];
    }

    /**
     * @param  array{fidelity: string, source: string, observed_at: ?string, observed_operation_run_id: ?int}  $baselineProvenance
     * @param  array<string, mixed>  $currentProvenance
     * @return array<string, mixed>
     */
    private function buildDriftEvidenceContract(
        string $changeType,
        string $policyType,
        string $subjectKey,
        ?string $displayName,
        ?string $baselineHash,
        ?string $currentHash,
        array $baselineProvenance,
        array $currentProvenance,
        ?int $baselinePolicyVersionId,
        ?int $currentPolicyVersionId,
        string $summaryKind,
        int $baselineProfileId,
        int $baselineSnapshotId,
        int $compareOperationRunId,
        int $inventorySyncRunId,
    ): array {
        $fidelity = $this->fidelityFromPolicyVersionRefs($baselinePolicyVersionId, $currentPolicyVersionId);

        return [
            'change_type' => $changeType,
            'policy_type' => $policyType,
            'subject_key' => $subjectKey,
            'display_name' => $displayName,
            'summary' => [
                'kind' => $summaryKind,
            ],
            'baseline' => [
                'policy_version_id' => $baselinePolicyVersionId,
                'hash' => $baselineHash,
                'provenance' => $baselineProvenance,
            ],
            'current' => [
                'policy_version_id' => $currentPolicyVersionId,
                'hash' => $currentHash,
                'provenance' => $currentProvenance,
            ],
            'fidelity' => $fidelity,
            'provenance' => [
                'baseline_profile_id' => $baselineProfileId,
                'baseline_snapshot_id' => $baselineSnapshotId,
                'compare_operation_run_id' => $compareOperationRunId,
                'inventory_sync_run_id' => $inventorySyncRunId,
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $baselineMeta
     * @param  array<string, mixed>  $currentMeta
     * @param  array{
     *   baseline: array<string, mixed>,
     *   current: array<string, mixed>,
     *   changed_keys: list<string>,
     *   metadata_keys: list<string>,
     *   permission_keys: list<string>,
     *   diff_kind: string,
     *   diff_fingerprint: string
     * }|null  $roleDefinitionDiff
     * @return array{
     *   diff_kind: string,
     *   diff_fingerprint: string,
     *   changed_keys: list<string>,
     *   metadata_keys: list<string>,
     *   permission_keys: list<string>,
     *   baseline: array{normalized: array<string, mixed>, is_built_in: mixed, role_permission_count: mixed},
     *   current: array{normalized: array<string, mixed>, is_built_in: mixed, role_permission_count: mixed}
     * }
     */
    private function buildRoleDefinitionEvidencePayload(
        Tenant $tenant,
        ?int $baselinePolicyVersionId,
        ?int $currentPolicyVersionId,
        array $baselineMeta,
        array $currentMeta,
        string $diffKind,
        ?array $roleDefinitionDiff = null,
    ): array {
        $baselineVersion = $this->resolveRoleDefinitionVersion($tenant, $baselinePolicyVersionId);
        $currentVersion = $this->resolveRoleDefinitionVersion($tenant, $currentPolicyVersionId);

        $baselineNormalized = is_array($roleDefinitionDiff['baseline'] ?? null)
            ? $roleDefinitionDiff['baseline']
            : $this->fallbackRoleDefinitionNormalized($baselineVersion, $baselineMeta);
        $currentNormalized = is_array($roleDefinitionDiff['current'] ?? null)
            ? $roleDefinitionDiff['current']
            : $this->fallbackRoleDefinitionNormalized($currentVersion, $currentMeta);

        $changedKeys = is_array($roleDefinitionDiff['changed_keys'] ?? null)
            ? array_values(array_filter($roleDefinitionDiff['changed_keys'], 'is_string'))
            : $this->roleDefinitionChangedKeys($baselineNormalized, $currentNormalized);
        $metadataKeys = is_array($roleDefinitionDiff['metadata_keys'] ?? null)
            ? array_values(array_filter($roleDefinitionDiff['metadata_keys'], 'is_string'))
            : array_values(array_diff($changedKeys, $this->roleDefinitionPermissionKeys($changedKeys)));
        $permissionKeys = is_array($roleDefinitionDiff['permission_keys'] ?? null)
            ? array_values(array_filter($roleDefinitionDiff['permission_keys'], 'is_string'))
            : $this->roleDefinitionPermissionKeys($changedKeys);

        $resolvedDiffKind = is_string($roleDefinitionDiff['diff_kind'] ?? null)
            ? (string) $roleDefinitionDiff['diff_kind']
            : $diffKind;
        $diffFingerprint = is_string($roleDefinitionDiff['diff_fingerprint'] ?? null)
            ? (string) $roleDefinitionDiff['diff_fingerprint']
            : hash(
                'sha256',
                json_encode([
                    'diff_kind' => $resolvedDiffKind,
                    'changed_keys' => $changedKeys,
                    'baseline' => $baselineNormalized,
                    'current' => $currentNormalized,
                ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            );

        return [
            'diff_kind' => $resolvedDiffKind,
            'diff_fingerprint' => $diffFingerprint,
            'changed_keys' => $changedKeys,
            'metadata_keys' => $metadataKeys,
            'permission_keys' => $permissionKeys,
            'baseline' => [
                'normalized' => $baselineNormalized,
                'is_built_in' => data_get($baselineMeta, 'rbac.is_built_in', data_get($baselineMeta, 'is_built_in')),
                'role_permission_count' => data_get($baselineMeta, 'rbac.role_permission_count', data_get($baselineMeta, 'role_permission_count')),
            ],
            'current' => [
                'normalized' => $currentNormalized,
                'is_built_in' => data_get($currentMeta, 'rbac.is_built_in', data_get($currentMeta, 'is_built_in')),
                'role_permission_count' => data_get($currentMeta, 'rbac.role_permission_count', data_get($currentMeta, 'role_permission_count')),
            ],
        ];
    }

    private function resolveRoleDefinitionVersion(Tenant $tenant, ?int $policyVersionId): ?PolicyVersion
    {
        if ($policyVersionId === null) {
            return null;
        }

        return PolicyVersion::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->find($policyVersionId);
    }

    /**
     * @param  array<string, mixed>  $meta
     * @return array<string, mixed>
     */
    private function fallbackRoleDefinitionNormalized(?PolicyVersion $version, array $meta): array
    {
        if ($version instanceof PolicyVersion) {
            return app(IntuneRoleDefinitionNormalizer::class)->flattenForDiff(
                is_array($version->snapshot) ? $version->snapshot : [],
                'intuneRoleDefinition',
                is_string($version->platform ?? null) ? (string) $version->platform : null,
            );
        }

        $normalized = [];
        $displayName = $meta['display_name'] ?? null;

        if (is_string($displayName) && trim($displayName) !== '') {
            $normalized['Role definition > Display name'] = trim($displayName);
        }

        $isBuiltIn = data_get($meta, 'rbac.is_built_in', data_get($meta, 'is_built_in'));
        if (is_bool($isBuiltIn)) {
            $normalized['Role definition > Role source'] = $isBuiltIn ? 'Built-in' : 'Custom';
        }

        $rolePermissionCount = data_get($meta, 'rbac.role_permission_count', data_get($meta, 'role_permission_count'));
        if (is_numeric($rolePermissionCount)) {
            $normalized['Role definition > Permission blocks'] = (int) $rolePermissionCount;
        }

        return $normalized;
    }

    /**
     * @param  array<string, mixed>  $baselineNormalized
     * @param  array<string, mixed>  $currentNormalized
     * @return list<string>
     */
    private function roleDefinitionChangedKeys(array $baselineNormalized, array $currentNormalized): array
    {
        $keys = array_values(array_unique(array_merge(array_keys($baselineNormalized), array_keys($currentNormalized))));
        sort($keys, SORT_STRING);

        return array_values(array_filter($keys, fn (string $key): bool => ($baselineNormalized[$key] ?? null) !== ($currentNormalized[$key] ?? null)));
    }

    /**
     * @param  list<string>  $keys
     * @return list<string>
     */
    private function roleDefinitionPermissionKeys(array $keys): array
    {
        return array_values(array_filter(
            $keys,
            fn (string $key): bool => str_starts_with($key, 'Permission block ')
        ));
    }

    private function fidelityFromPolicyVersionRefs(?int $baselinePolicyVersionId, ?int $currentPolicyVersionId): string
    {
        if ($baselinePolicyVersionId !== null && $currentPolicyVersionId !== null) {
            return 'content';
        }

        if ($baselinePolicyVersionId !== null || $currentPolicyVersionId !== null) {
            return 'mixed';
        }

        return 'meta';
    }

    private function normalizeSubjectKey(
        string $policyType,
        ?string $storedSubjectKey = null,
        ?string $displayName = null,
        ?string $subjectExternalId = null,
    ): string {
        $storedSubjectKey = is_string($storedSubjectKey) ? trim(mb_strtolower($storedSubjectKey)) : '';

        if ($storedSubjectKey !== '') {
            return $storedSubjectKey;
        }

        return BaselineSubjectKey::forPolicy($policyType, $displayName, $subjectExternalId) ?? '';
    }

    /**
     * @return array{
     *   baseline: array<string, mixed>,
     *   current: array<string, mixed>,
     *   changed_keys: list<string>,
     *   metadata_keys: list<string>,
     *   permission_keys: list<string>,
     *   diff_kind: string,
     *   diff_fingerprint: string
     * }|null
     */
    private function resolveRoleDefinitionDiff(
        Tenant $tenant,
        int $baselinePolicyVersionId,
        int $currentPolicyVersionId,
        IntuneRoleDefinitionNormalizer $normalizer,
    ): ?array {
        $baselineVersion = $this->resolveRoleDefinitionVersion($tenant, $baselinePolicyVersionId);
        $currentVersion = $this->resolveRoleDefinitionVersion($tenant, $currentPolicyVersionId);

        if (! $baselineVersion instanceof PolicyVersion || ! $currentVersion instanceof PolicyVersion) {
            return null;
        }

        return $normalizer->classifyDiff(
            baselineSnapshot: is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [],
            currentSnapshot: is_array($currentVersion->snapshot) ? $currentVersion->snapshot : [],
            platform: is_string($currentVersion->platform ?? null)
                ? (string) $currentVersion->platform
                : (is_string($baselineVersion->platform ?? null) ? (string) $baselineVersion->platform : null),
        );
    }

    /**
     * @param  array{diff_kind?: string}|null  $roleDefinitionDiff
     */
    private function severityForRoleDefinitionDiff(?array $roleDefinitionDiff): string
    {
        return match ($roleDefinitionDiff['diff_kind'] ?? null) {
            'metadata_only' => Finding::SEVERITY_LOW,
            default => Finding::SEVERITY_HIGH,
        };
    }

    /**
     * @return array{total_compared: int, unchanged: int, modified: int, missing: int, unexpected: int}
     */
    private function emptyRbacRoleDefinitionSummary(): array
    {
        return [
            'total_compared' => 0,
            'unchanged' => 0,
            'modified' => 0,
            'missing' => 0,
            'unexpected' => 0,
        ];
    }

    /**
     * @param  array<string, int>  ...$gaps
     * @return array<string, int>
     */
    private function mergeGapCounts(array ...$gaps): array
    {
        $merged = [];

        foreach ($gaps as $gap) {
            foreach ($gap as $reason => $count) {
                if (! is_string($reason) || ! is_numeric($count)) {
                    continue;
                }

                $count = (int) $count;

                if ($count <= 0) {
                    continue;
                }

                $merged[$reason] = ($merged[$reason] ?? 0) + $count;
            }
        }

        ksort($merged);

        return $merged;
    }

    /**
     * @param  array<string, array{subject_external_id: string, policy_type: string, meta_jsonb: array<string, mixed>}>  $currentItems
     * @param  array<string, ResolvedEvidence|null>  $resolvedCurrentEvidence
     * @return array{
     *   subjects_total: int,
     *   resolved_total: int,
     *   resolved_content: int,
     *   resolved_meta: int,
     *   policy_types_content: list<string>,
     *   policy_types_meta_only: list<string>
     * }
     */
    private function summarizeCurrentEvidenceCoverage(array $currentItems, array $resolvedCurrentEvidence): array
    {
        $subjectsTotal = count($currentItems);
        $resolvedContent = 0;
        $resolvedMeta = 0;

        /**
         * @var array<string, array{resolved_content: int, resolved_meta: int}>
         */
        $resolvedByType = [];

        foreach ($currentItems as $key => $item) {
            $type = (string) ($item['policy_type'] ?? '');

            if ($type === '') {
                continue;
            }

            $resolvedByType[$type] ??= ['resolved_content' => 0, 'resolved_meta' => 0];

            $evidence = $resolvedCurrentEvidence[$key] ?? null;

            if (! $evidence instanceof ResolvedEvidence) {
                continue;
            }

            if ($evidence->fidelity === EvidenceProvenance::FidelityContent) {
                $resolvedContent++;
                $resolvedByType[$type]['resolved_content']++;
            } else {
                $resolvedMeta++;
                $resolvedByType[$type]['resolved_meta']++;
            }
        }

        $policyTypesContent = [];
        $policyTypesMetaOnly = [];

        foreach ($resolvedByType as $policyType => $counts) {
            if ($counts['resolved_content'] > 0) {
                $policyTypesContent[] = $policyType;

                continue;
            }

            if ($counts['resolved_meta'] > 0) {
                $policyTypesMetaOnly[] = $policyType;
            }
        }

        sort($policyTypesContent, SORT_STRING);
        sort($policyTypesMetaOnly, SORT_STRING);

        return [
            'subjects_total' => $subjectsTotal,
            'resolved_total' => $resolvedContent + $resolvedMeta,
            'resolved_content' => $resolvedContent,
            'resolved_meta' => $resolvedMeta,
            'policy_types_content' => $policyTypesContent,
            'policy_types_meta_only' => $policyTypesMetaOnly,
        ];
    }

    /**
     * @param  array<string, array{subject_type: string, subject_external_id: string, policy_type: string, baseline_hash: string, meta_jsonb: array<string, mixed>}>  $baselineItems
     * @return array{
     *   baseline_total: int,
     *   baseline_content: int,
     *   baseline_meta: int,
     *   baseline_policy_types_content: list<string>,
     *   baseline_policy_types_meta_only: list<string>
     * }
     */
    private function summarizeBaselineEvidenceCoverage(array $baselineItems): array
    {
        $baselineTotal = count($baselineItems);
        $baselineContent = 0;
        $baselineMeta = 0;

        /**
         * @var array<string, array{baseline_content: int, baseline_meta: int}>
         */
        $countsByType = [];

        foreach ($baselineItems as $key => $item) {
            $type = (string) ($item['policy_type'] ?? '');

            if ($type === '') {
                continue;
            }

            $countsByType[$type] ??= ['baseline_content' => 0, 'baseline_meta' => 0];

            $provenance = $this->baselineProvenanceFromMetaJsonb($item['meta_jsonb'] ?? []);
            $fidelity = (string) ($provenance['fidelity'] ?? EvidenceProvenance::FidelityMeta);

            if ($fidelity === EvidenceProvenance::FidelityContent) {
                $baselineContent++;
                $countsByType[$type]['baseline_content']++;
            } else {
                $baselineMeta++;
                $countsByType[$type]['baseline_meta']++;
            }
        }

        $policyTypesContent = [];
        $policyTypesMetaOnly = [];

        foreach ($countsByType as $policyType => $counts) {
            if (($counts['baseline_content'] ?? 0) > 0) {
                $policyTypesContent[] = $policyType;

                continue;
            }

            if (($counts['baseline_meta'] ?? 0) > 0) {
                $policyTypesMetaOnly[] = $policyType;
            }
        }

        sort($policyTypesContent, SORT_STRING);
        sort($policyTypesMetaOnly, SORT_STRING);

        return [
            'baseline_total' => $baselineTotal,
            'baseline_content' => $baselineContent,
            'baseline_meta' => $baselineMeta,
            'baseline_policy_types_content' => $policyTypesContent,
            'baseline_policy_types_meta_only' => $policyTypesMetaOnly,
        ];
    }

    /**
     * @param  array<string, mixed>  $metaJsonb
     * @return array{fidelity: string, source: string, observed_at: ?string, observed_operation_run_id: ?int}
     */
    private function baselineProvenanceFromMetaJsonb(array $metaJsonb): array
    {
        $evidence = $metaJsonb;

        if (is_array($metaJsonb['evidence'] ?? null)) {
            $evidence = $metaJsonb['evidence'];
        }

        $fidelity = $evidence['fidelity'] ?? EvidenceProvenance::FidelityMeta;
        $fidelity = is_string($fidelity) ? strtolower(trim($fidelity)) : EvidenceProvenance::FidelityMeta;
        if (! EvidenceProvenance::isValidFidelity($fidelity)) {
            $fidelity = EvidenceProvenance::FidelityMeta;
        }

        $source = $evidence['source'] ?? EvidenceProvenance::SourceInventory;
        $source = is_string($source) ? strtolower(trim($source)) : EvidenceProvenance::SourceInventory;
        if (! EvidenceProvenance::isValidSource($source)) {
            $source = EvidenceProvenance::SourceInventory;
        }

        $observedAt = $evidence['observed_at'] ?? null;
        $observedAt = is_string($observedAt) ? trim($observedAt) : null;

        $observedAtCarbon = null;
        if (is_string($observedAt) && $observedAt !== '') {
            try {
                $observedAtCarbon = CarbonImmutable::parse($observedAt);
            } catch (\Throwable) {
                $observedAtCarbon = null;
            }
        }

        $observedOperationRunId = $evidence['observed_operation_run_id'] ?? null;
        $observedOperationRunId = is_numeric($observedOperationRunId) ? (int) $observedOperationRunId : null;

        return EvidenceProvenance::build(
            fidelity: $fidelity,
            source: $source,
            observedAt: $observedAtCarbon,
            observedOperationRunId: $observedOperationRunId,
        );
    }

    /**
     * Upsert drift findings using stable fingerprints.
     *
     * @param  array<int, array{change_type: string, severity: string, subject_type: string, subject_external_id: string, subject_key: string, policy_type: string, baseline_hash: string, current_hash: string, evidence: array<string, mixed>}>  $driftResults
     * @return array{processed_count: int, created_count: int, reopened_count: int, unchanged_count: int, seen_fingerprints: array<int, string>}
     */
    private function upsertFindings(
        Tenant $tenant,
        BaselineProfile $profile,
        string $scopeKey,
        array $driftResults,
    ): array {
        $tenantId = (int) $tenant->getKey();
        $baselineProfileId = (int) $profile->getKey();
        $observedAt = CarbonImmutable::now();
        $processedCount = 0;
        $createdCount = 0;
        $reopenedCount = 0;
        $unchangedCount = 0;
        $seenFingerprints = [];
        $slaPolicy = app(FindingSlaPolicy::class);

        foreach ($driftResults as $driftItem) {
            $subjectKey = (string) ($driftItem['subject_key'] ?? '');

            if (trim($subjectKey) === '') {
                continue;
            }

            $recurrenceKey = $this->recurrenceKey(
                tenantId: $tenantId,
                baselineProfileId: $baselineProfileId,
                policyType: (string) ($driftItem['policy_type'] ?? ''),
                subjectKey: $subjectKey,
                changeType: (string) ($driftItem['change_type'] ?? ''),
            );

            $fingerprint = $recurrenceKey;

            $seenFingerprints[] = $fingerprint;

            $finding = Finding::query()
                ->where('tenant_id', $tenantId)
                ->where('fingerprint', $fingerprint)
                ->first();

            $isNewFinding = ! $finding instanceof Finding;

            if ($isNewFinding) {
                $finding = new Finding;
            } else {
                $this->observeFinding(
                    finding: $finding,
                    observedAt: $observedAt,
                    currentOperationRunId: (int) $this->operationRun->getKey(),
                );
            }

            $finding->forceFill([
                'tenant_id' => $tenantId,
                'finding_type' => Finding::FINDING_TYPE_DRIFT,
                'source' => 'baseline.compare',
                'scope_key' => $scopeKey,
                'subject_type' => $driftItem['subject_type'],
                'subject_external_id' => $driftItem['subject_external_id'],
                'severity' => $driftItem['severity'],
                'fingerprint' => $fingerprint,
                'recurrence_key' => $recurrenceKey,
                'evidence_jsonb' => $driftItem['evidence'],
                'evidence_fidelity' => $driftItem['evidence_fidelity'] ?? EvidenceProvenance::FidelityMeta,
                'baseline_operation_run_id' => null,
                'current_operation_run_id' => (int) $this->operationRun->getKey(),
            ]);

            if ($isNewFinding) {
                $severity = (string) $driftItem['severity'];
                $slaDays = $slaPolicy->daysForSeverity($severity, $tenant);

                $finding->forceFill([
                    'status' => Finding::STATUS_NEW,
                    'reopened_at' => null,
                    'resolved_at' => null,
                    'resolved_reason' => null,
                    'acknowledged_at' => null,
                    'acknowledged_by_user_id' => null,
                    'first_seen_at' => $observedAt,
                    'last_seen_at' => $observedAt,
                    'times_seen' => 1,
                    'sla_days' => $slaDays,
                    'due_at' => $slaPolicy->dueAtForSeverity($severity, $tenant, $observedAt),
                ]);

                $createdCount++;
            } elseif ((string) $finding->status === Finding::STATUS_RESOLVED) {
                $resolvedAt = $finding->resolved_at !== null
                    ? CarbonImmutable::instance($finding->resolved_at)
                    : null;

                if ($resolvedAt === null || $observedAt->greaterThan($resolvedAt)) {
                    $severity = (string) $driftItem['severity'];
                    $slaDays = $slaPolicy->daysForSeverity($severity, $tenant);

                    $finding->forceFill([
                        'status' => Finding::STATUS_REOPENED,
                        'reopened_at' => $observedAt,
                        'resolved_at' => null,
                        'resolved_reason' => null,
                        'closed_at' => null,
                        'closed_reason' => null,
                        'closed_by_user_id' => null,
                        'sla_days' => $slaDays,
                        'due_at' => $slaPolicy->dueAtForSeverity($severity, $tenant, $observedAt),
                    ]);

                    $reopenedCount++;
                } else {
                    $unchangedCount++;
                }
            } else {
                $unchangedCount++;
            }

            $finding->save();
            $processedCount++;
        }

        return [
            'processed_count' => $processedCount,
            'created_count' => $createdCount,
            'reopened_count' => $reopenedCount,
            'unchanged_count' => $unchangedCount,
            'seen_fingerprints' => array_values(array_unique($seenFingerprints)),
        ];
    }

    private function observeFinding(Finding $finding, CarbonImmutable $observedAt, int $currentOperationRunId): void
    {
        if ($finding->first_seen_at === null) {
            $finding->first_seen_at = $observedAt;
        }

        if ($finding->last_seen_at === null || $observedAt->greaterThan(CarbonImmutable::instance($finding->last_seen_at))) {
            $finding->last_seen_at = $observedAt;
        }

        $timesSeen = is_numeric($finding->times_seen) ? (int) $finding->times_seen : 0;

        if ((int) ($finding->current_operation_run_id ?? 0) !== $currentOperationRunId) {
            $finding->times_seen = max(0, $timesSeen) + 1;
        } elseif ($timesSeen < 1) {
            $finding->times_seen = 1;
        }
    }

    /**
     * Stable identity for baseline-compare findings, scoped to a baseline profile + subject key.
     */
    private function recurrenceKey(
        int $tenantId,
        int $baselineProfileId,
        string $policyType,
        string $subjectKey,
        string $changeType,
    ): string {
        $parts = [
            (string) $tenantId,
            (string) $baselineProfileId,
            $this->normalizeKeyPart($policyType),
            $this->normalizeKeyPart($subjectKey),
            $this->normalizeKeyPart($changeType),
        ];

        return hash('sha256', implode('|', $parts));
    }

    private function normalizeKeyPart(string $value): string
    {
        return trim(mb_strtolower($value));
    }

    /**
     * @param  array<int, array{change_type: string}>  $driftResults
     * @return array<string, int>
     */
    private function countByChangeType(array $driftResults): array
    {
        $counts = [];

        foreach ($driftResults as $item) {
            $changeType = (string) ($item['change_type'] ?? '');

            if ($changeType === '') {
                continue;
            }

            $counts[$changeType] = ($counts[$changeType] ?? 0) + 1;
        }

        ksort($counts);

        return $counts;
    }

    /**
     * @param  array<int, array{severity: string}>  $driftResults
     * @return array<string, int>
     */
    private function countBySeverity(array $driftResults): array
    {
        $counts = [];

        foreach ($driftResults as $item) {
            $severity = $item['severity'];
            $counts[$severity] = ($counts[$severity] ?? 0) + 1;
        }

        return $counts;
    }

    /**
     * @return array<string, string>
     */
    private function resolveSeverityMapping(Workspace $workspace, SettingsResolver $settingsResolver): array
    {
        try {
            $mapping = $settingsResolver->resolveValue(
                workspace: $workspace,
                domain: 'baseline',
                key: 'severity_mapping',
            );
        } catch (\InvalidArgumentException) {
            // Settings keys are registry-backed; if this key is missing (e.g. during rollout),
            // fall back to built-in defaults rather than failing the entire compare run.
            return [];
        }

        return is_array($mapping) ? $mapping : [];
    }

    /**
     * @param  array<string, string>  $severityMapping
     */
    private function severityForChangeType(array $severityMapping, string $changeType): string
    {
        $severity = $severityMapping[$changeType] ?? null;

        if (! is_string($severity) || $severity === '') {
            return match ($changeType) {
                'missing_policy' => Finding::SEVERITY_HIGH,
                'different_version' => Finding::SEVERITY_MEDIUM,
                default => Finding::SEVERITY_LOW,
            };
        }

        return $severity;
    }

    private function auditStarted(
        AuditLogger $auditLogger,
        Tenant $tenant,
        BaselineProfile $profile,
        ?User $initiator,
        BaselineCaptureMode $captureMode,
        int $subjectsTotal,
        BaselineScope $effectiveScope,
    ): void {
        $auditLogger->log(
            tenant: $tenant,
            action: 'baseline.compare.started',
            context: [
                'metadata' => [
                    'operation_run_id' => (int) $this->operationRun->getKey(),
                    'baseline_profile_id' => (int) $profile->getKey(),
                    'baseline_profile_name' => (string) $profile->name,
                    'purpose' => PolicyVersionCapturePurpose::BaselineCompare->value,
                    'capture_mode' => $captureMode->value,
                    'scope_types_total' => count($effectiveScope->allTypes()),
                    'subjects_total' => $subjectsTotal,
                ],
            ],
            actorId: $initiator?->id,
            actorEmail: $initiator?->email,
            actorName: $initiator?->name,
            resourceType: 'baseline_profile',
            resourceId: (string) $profile->getKey(),
        );
    }

    private function auditCompleted(
        AuditLogger $auditLogger,
        Tenant $tenant,
        BaselineProfile $profile,
        ?User $initiator,
        BaselineCaptureMode $captureMode,
        int $subjectsTotal,
        array $evidenceCaptureStats,
        array $gaps,
        array $summaryCounts,
    ): void {
        $auditLogger->log(
            tenant: $tenant,
            action: 'baseline.compare.completed',
            context: [
                'metadata' => [
                    'operation_run_id' => (int) $this->operationRun->getKey(),
                    'baseline_profile_id' => (int) $profile->getKey(),
                    'baseline_profile_name' => (string) $profile->name,
                    'purpose' => PolicyVersionCapturePurpose::BaselineCompare->value,
                    'capture_mode' => $captureMode->value,
                    'subjects_total' => $subjectsTotal,
                    'findings_total' => $summaryCounts['total'] ?? 0,
                    'high' => $summaryCounts['high'] ?? 0,
                    'medium' => $summaryCounts['medium'] ?? 0,
                    'low' => $summaryCounts['low'] ?? 0,
                    'evidence_capture' => $evidenceCaptureStats,
                    'gaps' => $gaps,
                ],
            ],
            actorId: $initiator?->id,
            actorEmail: $initiator?->email,
            actorName: $initiator?->name,
            resourceType: 'operation_run',
            resourceId: (string) $this->operationRun->getKey(),
        );
    }
}
