<?php

declare(strict_types=1);

namespace App\Support\Baselines\Compare;

use App\Models\Finding;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Services\Baselines\Evidence\BaselinePolicyVersionResolver;
use App\Services\Baselines\Evidence\ContentEvidenceProvider;
use App\Services\Baselines\Evidence\EvidenceProvenance;
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\Intune\IntuneRoleDefinitionNormalizer;
use App\Support\Baselines\OperatorActionCategory;
use App\Support\Baselines\ResolutionOutcome;
use App\Support\Baselines\ResolutionPath;
use App\Support\Baselines\SubjectClass;
use App\Support\Baselines\SubjectResolver;
use App\Support\Governance\GovernanceDomainKey;
use App\Support\Governance\GovernanceSubjectClass;
use App\Support\Governance\PlatformSubjectDescriptorNormalizer;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use App\Support\Ui\OperatorExplanation\TrustworthinessLevel;

final class IntuneCompareStrategy implements CompareStrategy
{
    public function __construct(
        private readonly BaselinePolicyVersionResolver $baselinePolicyVersionResolver,
        private readonly DriftHasher $hasher,
        private readonly SettingsNormalizer $settingsNormalizer,
        private readonly AssignmentsNormalizer $assignmentsNormalizer,
        private readonly ScopeTagsNormalizer $scopeTagsNormalizer,
        private readonly ContentEvidenceProvider $contentEvidenceProvider,
        private readonly IntuneRoleDefinitionNormalizer $roleDefinitionNormalizer,
        private readonly SubjectResolver $subjectResolver,
    ) {}

    public function key(): CompareStrategyKey
    {
        return CompareStrategyKey::intunePolicy();
    }

    public function capabilities(): array
    {
        return [
            new CompareStrategyCapability(
                strategyKey: $this->key(),
                domainKeys: [GovernanceDomainKey::Intune->value],
                subjectClasses: [GovernanceSubjectClass::Policy->value],
                subjectTypeKeys: 'all',
            ),
            new CompareStrategyCapability(
                strategyKey: $this->key(),
                domainKeys: [GovernanceDomainKey::PlatformFoundation->value],
                subjectClasses: [GovernanceSubjectClass::ConfigurationResource->value],
                subjectTypeKeys: 'all',
            ),
        ];
    }

    public function compare(
        CompareOrchestrationContext $context,
        ManagedEnvironment $tenant,
        array $baselineItems,
        array $currentItems,
        array $resolvedCurrentEvidence,
        array $severityMapping,
    ): array {
        $subjectResults = [];
        $rbacRoleDefinitionSummary = $this->emptyRbacRoleDefinitionSummary();
        $inventorySyncRunId = $context->inventorySyncRunId() ?? 0;

        $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 > 0 ? $inventorySyncRunId : null,
        );

        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,
            );
            $baselineComparableHash = $this->effectiveBaselineHash(
                tenant: $tenant,
                baselineItem: $baselineItem,
                baselinePolicyVersionId: $baselinePolicyVersionId,
            );

            if (! is_array($currentItem)) {
                if ($isRbacRoleDefinition && $baselinePolicyVersionId === null) {
                    $subjectResults[] = $this->gapResult(
                        policyType: $policyType,
                        subjectKey: $subjectKey,
                        subjectExternalId: (string) ($baselineItem['subject_external_id'] ?? ''),
                        operatorLabel: $this->operatorLabel($baselineItem, null),
                        compareState: CompareState::Incomplete,
                        reasonCode: 'missing_role_definition_baseline_version_reference',
                        baselineAvailability: 'available',
                        currentStateAvailability: 'missing',
                        trustLevel: TrustworthinessLevel::Unusable->value,
                        evidenceQuality: 'missing',
                    );

                    continue;
                }

                $evidence = $this->buildDriftEvidenceContract(
                    changeType: 'missing_policy',
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    displayName: $this->operatorLabel($baselineItem, null),
                    baselineHash: $baselineComparableHash,
                    currentHash: null,
                    baselineProvenance: $baselineProvenance,
                    currentProvenance: $currentMissingProvenance,
                    baselinePolicyVersionId: $baselinePolicyVersionId,
                    currentPolicyVersionId: null,
                    summaryKind: 'policy_snapshot',
                    baselineProfileId: $context->baselineProfileId,
                    baselineSnapshotId: $context->baselineSnapshotId,
                    compareOperationRunId: $context->operationRunId,
                    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',
                    );
                    $rbacRoleDefinitionSummary['missing']++;
                    $rbacRoleDefinitionSummary['total_compared']++;
                }

                $subjectResults[] = $this->driftResult(
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    subjectExternalId: (string) ($baselineItem['subject_external_id'] ?? ''),
                    subjectType: is_string($baselineItem['subject_type'] ?? null) ? (string) $baselineItem['subject_type'] : null,
                    operatorLabel: $this->operatorLabel($baselineItem, null),
                    changeType: 'missing_policy',
                    severity: $isRbacRoleDefinition
                        ? Finding::SEVERITY_HIGH
                        : $this->severityForChangeType($severityMapping, 'missing_policy'),
                    evidence: $evidence,
                    baselineAvailability: 'available',
                    currentStateAvailability: 'missing',
                );

                continue;
            }

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

            if (! $currentEvidence instanceof ResolvedEvidence) {
                $subjectResults[] = $this->gapResult(
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    subjectExternalId: (string) ($currentItem['subject_external_id'] ?? $baselineItem['subject_external_id'] ?? ''),
                    operatorLabel: $this->operatorLabel($baselineItem, $currentItem),
                    compareState: CompareState::Incomplete,
                    reasonCode: 'missing_current',
                    baselineAvailability: 'available',
                    currentStateAvailability: 'unknown',
                    trustLevel: TrustworthinessLevel::Unusable->value,
                    evidenceQuality: 'missing',
                );

                continue;
            }

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

            if ($baselineComparableHash !== $currentEvidence->hash) {
                $roleDefinitionDiff = null;

                if ($isRbacRoleDefinition) {
                    if ($baselinePolicyVersionId === null) {
                        $subjectResults[] = $this->gapResult(
                            policyType: $policyType,
                            subjectKey: $subjectKey,
                            subjectExternalId: (string) ($currentItem['subject_external_id'] ?? $baselineItem['subject_external_id'] ?? ''),
                            operatorLabel: $this->operatorLabel($baselineItem, $currentItem),
                            compareState: CompareState::Incomplete,
                            reasonCode: 'missing_role_definition_baseline_version_reference',
                            baselineAvailability: 'available',
                            currentStateAvailability: 'available',
                            trustLevel: TrustworthinessLevel::Unusable->value,
                            evidenceQuality: $currentEvidence->fidelity,
                        );

                        continue;
                    }

                    if ($currentPolicyVersionId === null) {
                        $subjectResults[] = $this->gapResult(
                            policyType: $policyType,
                            subjectKey: $subjectKey,
                            subjectExternalId: (string) ($currentItem['subject_external_id'] ?? $baselineItem['subject_external_id'] ?? ''),
                            operatorLabel: $this->operatorLabel($baselineItem, $currentItem),
                            compareState: CompareState::Incomplete,
                            reasonCode: 'missing_role_definition_current_version_reference',
                            baselineAvailability: 'available',
                            currentStateAvailability: 'available',
                            trustLevel: TrustworthinessLevel::Unusable->value,
                            evidenceQuality: $currentEvidence->fidelity,
                        );

                        continue;
                    }

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

                    if (! is_array($roleDefinitionDiff)) {
                        $subjectResults[] = $this->gapResult(
                            policyType: $policyType,
                            subjectKey: $subjectKey,
                            subjectExternalId: (string) ($currentItem['subject_external_id'] ?? $baselineItem['subject_external_id'] ?? ''),
                            operatorLabel: $this->operatorLabel($baselineItem, $currentItem),
                            compareState: CompareState::Incomplete,
                            reasonCode: 'missing_role_definition_compare_surface',
                            baselineAvailability: 'available',
                            currentStateAvailability: 'available',
                            trustLevel: TrustworthinessLevel::Unusable->value,
                            evidenceQuality: $currentEvidence->fidelity,
                        );

                        continue;
                    }
                }

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

                $evidence = $this->buildDriftEvidenceContract(
                    changeType: 'different_version',
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    displayName: $this->operatorLabel($baselineItem, $currentItem),
                    baselineHash: $baselineComparableHash,
                    currentHash: (string) $currentEvidence->hash,
                    baselineProvenance: $baselineProvenance,
                    currentProvenance: $currentEvidence->tenantProvenance(),
                    baselinePolicyVersionId: $baselinePolicyVersionId,
                    currentPolicyVersionId: $currentPolicyVersionId,
                    summaryKind: $summaryKind,
                    baselineProfileId: $context->baselineProfileId,
                    baselineSnapshotId: $context->baselineSnapshotId,
                    compareOperationRunId: $context->operationRunId,
                    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']++;
                }

                $subjectResults[] = $this->driftResult(
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    subjectExternalId: (string) ($currentItem['subject_external_id'] ?? $baselineItem['subject_external_id'] ?? ''),
                    subjectType: is_string($baselineItem['subject_type'] ?? null) ? (string) $baselineItem['subject_type'] : null,
                    operatorLabel: $this->operatorLabel($baselineItem, $currentItem),
                    changeType: 'different_version',
                    severity: $isRbacRoleDefinition
                        ? $this->severityForRoleDefinitionDiff($roleDefinitionDiff)
                        : $this->severityForChangeType($severityMapping, 'different_version'),
                    evidence: $evidence,
                    baselineAvailability: 'available',
                    currentStateAvailability: 'available',
                );

                continue;
            }

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

            $subjectResults[] = new CompareSubjectResult(
                subjectIdentity: $this->subjectIdentity($policyType, (string) ($currentItem['subject_external_id'] ?? $baselineItem['subject_external_id'] ?? ''), $subjectKey),
                projection: $this->subjectProjection($policyType, $this->operatorLabel($baselineItem, $currentItem), is_string($baselineItem['subject_type'] ?? null) ? (string) $baselineItem['subject_type'] : null, 'policy_snapshot'),
                baselineAvailability: 'available',
                currentStateAvailability: 'available',
                compareState: CompareState::NoDrift,
                trustLevel: $currentEvidence->fidelity === EvidenceProvenance::FidelityContent
                    ? TrustworthinessLevel::Trustworthy->value
                    : TrustworthinessLevel::LimitedConfidence->value,
                evidenceQuality: $currentEvidence->fidelity,
                diagnostics: [
                    'strategy_key' => $this->key()->value,
                ],
            );
        }

        foreach ($currentItems as $key => $currentItem) {
            if (array_key_exists($key, $baselineItems)) {
                continue;
            }

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

            if (! $currentEvidence instanceof ResolvedEvidence) {
                $subjectResults[] = $this->gapResult(
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    subjectExternalId: (string) ($currentItem['subject_external_id'] ?? ''),
                    operatorLabel: $this->operatorLabel(null, $currentItem),
                    compareState: CompareState::Incomplete,
                    reasonCode: 'missing_current',
                    baselineAvailability: 'missing',
                    currentStateAvailability: 'unknown',
                    trustLevel: TrustworthinessLevel::Unusable->value,
                    evidenceQuality: 'missing',
                );

                continue;
            }

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

            if ($isRbacRoleDefinition && $currentPolicyVersionId === null) {
                $subjectResults[] = $this->gapResult(
                    policyType: $policyType,
                    subjectKey: $subjectKey,
                    subjectExternalId: (string) ($currentItem['subject_external_id'] ?? ''),
                    operatorLabel: $this->operatorLabel(null, $currentItem),
                    compareState: CompareState::Incomplete,
                    reasonCode: 'missing_role_definition_current_version_reference',
                    baselineAvailability: 'missing',
                    currentStateAvailability: 'available',
                    trustLevel: TrustworthinessLevel::Unusable->value,
                    evidenceQuality: $currentEvidence->fidelity,
                );

                continue;
            }

            $evidence = $this->buildDriftEvidenceContract(
                changeType: 'unexpected_policy',
                policyType: $policyType,
                subjectKey: $subjectKey,
                displayName: $this->operatorLabel(null, $currentItem),
                baselineHash: null,
                currentHash: (string) $currentEvidence->hash,
                baselineProvenance: $baselinePlaceholderProvenance,
                currentProvenance: $currentEvidence->tenantProvenance(),
                baselinePolicyVersionId: null,
                currentPolicyVersionId: $currentPolicyVersionId,
                summaryKind: 'policy_snapshot',
                baselineProfileId: $context->baselineProfileId,
                baselineSnapshotId: $context->baselineSnapshotId,
                compareOperationRunId: $context->operationRunId,
                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',
                );
                $rbacRoleDefinitionSummary['unexpected']++;
                $rbacRoleDefinitionSummary['total_compared']++;
            }

            $subjectResults[] = $this->driftResult(
                policyType: $policyType,
                subjectKey: $subjectKey,
                subjectExternalId: (string) ($currentItem['subject_external_id'] ?? ''),
                subjectType: null,
                operatorLabel: $this->operatorLabel(null, $currentItem),
                changeType: 'unexpected_policy',
                severity: $isRbacRoleDefinition
                    ? Finding::SEVERITY_MEDIUM
                    : $this->severityForChangeType($severityMapping, 'unexpected_policy'),
                evidence: $evidence,
                baselineAvailability: 'missing',
                currentStateAvailability: 'available',
            );
        }

        return [
            'subject_results' => $subjectResults,
            'diagnostics' => [
                'rbac_role_definitions' => $rbacRoleDefinitionSummary,
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $baselineItem
     * @param  array<string, mixed>|null  $currentItem
     * @param  array<string, mixed>  $evidence
     */
    private function driftResult(
        string $policyType,
        string $subjectKey,
        string $subjectExternalId,
        ?string $subjectType,
        string $operatorLabel,
        string $changeType,
        string $severity,
        array $evidence,
        string $baselineAvailability,
        string $currentStateAvailability,
    ): CompareSubjectResult {
        $fidelity = is_string($evidence['fidelity'] ?? null) && trim((string) $evidence['fidelity']) !== ''
            ? trim((string) $evidence['fidelity'])
            : EvidenceProvenance::FidelityMeta;

        return new CompareSubjectResult(
            subjectIdentity: $this->subjectIdentity($policyType, $subjectExternalId, $subjectKey),
            projection: $this->subjectProjection(
                policyType: $policyType,
                operatorLabel: $operatorLabel,
                subjectType: $subjectType,
                summaryKind: is_string(data_get($evidence, 'summary.kind')) ? (string) data_get($evidence, 'summary.kind') : null,
            ),
            baselineAvailability: $baselineAvailability,
            currentStateAvailability: $currentStateAvailability,
            compareState: CompareState::Drift,
            trustLevel: $fidelity === EvidenceProvenance::FidelityContent
                ? TrustworthinessLevel::Trustworthy->value
                : TrustworthinessLevel::LimitedConfidence->value,
            evidenceQuality: $fidelity,
            severityRecommendation: $severity,
            findingCandidate: new CompareFindingCandidate(
                changeType: $changeType,
                severity: $severity,
                fingerprintBasis: [
                    'policy_type' => $policyType,
                    'subject_key' => $subjectKey,
                    'change_type' => $changeType,
                ],
                evidencePayload: $evidence,
            ),
            diagnostics: [
                'strategy_key' => $this->key()->value,
            ],
        );
    }

    private function gapResult(
        string $policyType,
        string $subjectKey,
        string $subjectExternalId,
        string $operatorLabel,
        CompareState $compareState,
        string $reasonCode,
        string $baselineAvailability,
        string $currentStateAvailability,
        string $trustLevel,
        string $evidenceQuality,
    ): CompareSubjectResult {
        $descriptor = $this->subjectResolver->describeForCompare(
            policyType: $policyType,
            subjectExternalId: $subjectExternalId !== '' ? $subjectExternalId : null,
            subjectKey: $subjectKey,
        );

        $outcome = match ($reasonCode) {
            'missing_current' => $this->subjectResolver->missingExpectedRecord($descriptor),
            'ambiguous_match' => $this->subjectResolver->ambiguousMatch($descriptor),
            default => $this->subjectResolver->captureFailed($descriptor),
        };

        $gapRecord = array_merge($descriptor->toArray(), $outcome->toArray(), [
            'reason_code' => $reasonCode,
            'search_text' => strtolower(implode(' ', array_filter([$policyType, $subjectKey, $reasonCode, $operatorLabel]))),
        ]);

        return new CompareSubjectResult(
            subjectIdentity: $this->subjectIdentity($policyType, $subjectExternalId, $subjectKey),
            projection: $this->subjectProjection($policyType, $operatorLabel, null, null),
            baselineAvailability: $baselineAvailability,
            currentStateAvailability: $currentStateAvailability,
            compareState: $compareState,
            trustLevel: $trustLevel,
            evidenceQuality: $evidenceQuality,
            diagnostics: [
                'strategy_key' => $this->key()->value,
                'reason_code' => $reasonCode,
                'gap_record' => $gapRecord,
            ],
        );
    }

    private function subjectIdentity(string $policyType, string $subjectExternalId, string $subjectKey): CompareSubjectIdentity
    {
        return new CompareSubjectIdentity(
            domainKey: $this->domainKeyFor($policyType),
            subjectClass: $this->subjectClassFor($policyType),
            subjectTypeKey: $policyType,
            externalSubjectId: $subjectExternalId !== '' ? $subjectExternalId : null,
            subjectKey: $subjectKey,
        );
    }

    private function subjectProjection(string $policyType, string $operatorLabel, ?string $subjectType, ?string $summaryKind): CompareSubjectProjection
    {
        return new CompareSubjectProjection(
            platformSubjectClass: $this->platformSubjectClassFor($policyType, $subjectType),
            domainKey: $this->domainKeyFor($policyType),
            subjectTypeKey: $policyType,
            operatorLabel: $operatorLabel,
            summaryKind: $summaryKind,
            additionalLabels: [
                'policy_type_label' => InventoryPolicyTypeMeta::baselineCompareLabel($policyType) ?? $policyType,
                'governed_subject_label' => (string) data_get($this->subjectDescriptor($policyType), 'display_label', $policyType),
            ],
            subjectDescriptor: $this->subjectDescriptor($policyType),
        );
    }

    /**
     * @return array<string, mixed>
     */
    private function subjectDescriptor(string $policyType): array
    {
        static $cache = [];

        if (array_key_exists($policyType, $cache)) {
            return $cache[$policyType];
        }

        $result = app(PlatformSubjectDescriptorNormalizer::class)->fromArray([
            'policy_type' => $policyType,
        ], 'baseline_compare');

        return $cache[$policyType] = $result->descriptor->toArray();
    }

    private function domainKeyFor(string $policyType): string
    {
        return InventoryPolicyTypeMeta::isFoundation($policyType)
            ? GovernanceDomainKey::PlatformFoundation->value
            : GovernanceDomainKey::Intune->value;
    }

    private function subjectClassFor(string $policyType): string
    {
        return InventoryPolicyTypeMeta::isFoundation($policyType)
            ? GovernanceSubjectClass::ConfigurationResource->value
            : GovernanceSubjectClass::Policy->value;
    }

    private function platformSubjectClassFor(string $policyType, ?string $subjectType): string
    {
        if (is_string($subjectType) && trim($subjectType) !== '') {
            return trim($subjectType);
        }

        return InventoryPolicyTypeMeta::isFoundation($policyType)
            ? GovernanceSubjectClass::ConfigurationResource->value
            : 'policy';
    }

    /**
     * @param  array<string, mixed>|null  $baselineItem
     * @param  array<string, mixed>|null  $currentItem
     */
    private function operatorLabel(?array $baselineItem, ?array $currentItem): string
    {
        $displayName = $currentItem['meta_jsonb']['display_name']
            ?? $baselineItem['meta_jsonb']['display_name']
            ?? $currentItem['subject_key']
            ?? $baselineItem['subject_key']
            ?? 'Unknown subject';

        return is_string($displayName) && trim($displayName) !== '' ? trim($displayName) : 'Unknown subject';
    }

    private function effectiveBaselineHash(ManagedEnvironment $tenant, array $baselineItem, ?int $baselinePolicyVersionId): string
    {
        $storedHash = (string) ($baselineItem['baseline_hash'] ?? '');

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

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

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

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

    private function resolveBaselinePolicyVersionId(ManagedEnvironment $tenant, array $baselineItem, array $baselineProvenance): ?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 $this->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(ManagedEnvironment $tenant, string $policyType, ?int $baselinePolicyVersionId, ?int $currentPolicyVersionId): string
    {
        if ($baselinePolicyVersionId === null || $currentPolicyVersionId === null) {
            return 'policy_snapshot';
        }

        $baselineVersion = PolicyVersion::query()
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->find($baselinePolicyVersionId);
        $currentVersion = PolicyVersion::query()
            ->where('managed_environment_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);

        $baselineSnapshotHash = $this->hasher->hashNormalized([
            'settings' => $this->settingsNormalizer->normalizeForDiff(
                snapshot: is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [],
                policyType: $policyType,
                platform: $platform,
            ),
            'secret_fingerprints' => $this->fingerprintBucket($baselineVersion, 'snapshot'),
        ]);
        $currentSnapshotHash = $this->hasher->hashNormalized([
            'settings' => $this->settingsNormalizer->normalizeForDiff(
                snapshot: is_array($currentVersion->snapshot) ? $currentVersion->snapshot : [],
                policyType: $policyType,
                platform: $platform,
            ),
            'secret_fingerprints' => $this->fingerprintBucket($currentVersion, 'snapshot'),
        ]);

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

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

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

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

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

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

        return $baselineScopeTagsHash !== $currentScopeTagsHash
            ? 'policy_scope_tags'
            : 'policy_snapshot';
    }

    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 fingerprintBucket(PolicyVersion $version, string $bucket): array
    {
        $secretFingerprints = is_array($version->secret_fingerprints) ? $version->secret_fingerprints : [];
        $bucketFingerprints = $secretFingerprints[$bucket] ?? [];

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

    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 {
        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' => $this->fidelityFromPolicyVersionRefs($baselinePolicyVersionId, $currentPolicyVersionId),
            'provenance' => [
                'baseline_profile_id' => $baselineProfileId,
                'baseline_snapshot_id' => $baselineSnapshotId,
                'compare_operation_run_id' => $compareOperationRunId,
                'inventory_sync_run_id' => $inventorySyncRunId,
            ],
        ];
    }

    private function buildRoleDefinitionEvidencePayload(
        ManagedEnvironment $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;

        return [
            'diff_kind' => $resolvedDiffKind,
            'diff_fingerprint' => 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),
                ),
            '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(ManagedEnvironment $tenant, ?int $policyVersionId): ?PolicyVersion
    {
        if ($policyVersionId === null) {
            return null;
        }

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

    private function fallbackRoleDefinitionNormalized(?PolicyVersion $version, array $meta): array
    {
        if ($version instanceof PolicyVersion) {
            return $this->roleDefinitionNormalizer->buildEvidenceMap(
                is_array($version->snapshot) ? $version->snapshot : [],
                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;
    }

    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),
        ));
    }

    private function roleDefinitionPermissionKeys(array $keys): array
    {
        return array_values(array_filter(
            $keys,
            static 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 resolveRoleDefinitionDiff(ManagedEnvironment $tenant, int $baselinePolicyVersionId, int $currentPolicyVersionId): ?array
    {
        $baselineVersion = $this->resolveRoleDefinitionVersion($tenant, $baselinePolicyVersionId);
        $currentVersion = $this->resolveRoleDefinitionVersion($tenant, $currentPolicyVersionId);

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

        return $this->roleDefinitionNormalizer->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),
        );
    }

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

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

    private function baselineProvenanceFromMetaJsonb(array $metaJsonb): array
    {
        $evidence = is_array($metaJsonb['evidence'] ?? null) ? $metaJsonb['evidence'] : $metaJsonb;
        $fidelity = is_string($evidence['fidelity'] ?? null) ? strtolower(trim((string) $evidence['fidelity'])) : EvidenceProvenance::FidelityMeta;
        $source = is_string($evidence['source'] ?? null) ? strtolower(trim((string) $evidence['source'])) : EvidenceProvenance::SourceInventory;
        $observedAt = is_string($evidence['observed_at'] ?? null) ? trim((string) $evidence['observed_at']) : null;
        $observedAtCarbon = null;

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

        $observedOperationRunId = $evidence['observed_operation_run_id'] ?? null;

        return EvidenceProvenance::build(
            fidelity: EvidenceProvenance::isValidFidelity($fidelity) ? $fidelity : EvidenceProvenance::FidelityMeta,
            source: EvidenceProvenance::isValidSource($source) ? $source : EvidenceProvenance::SourceInventory,
            observedAt: $observedAtCarbon,
            observedOperationRunId: is_numeric($observedOperationRunId) ? (int) $observedOperationRunId : null,
        );
    }
}