<?php

namespace App\Jobs;

use App\Jobs\Concerns\BridgesFailedOperationRun;
use App\Jobs\Middleware\TrackOperationRun;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineSnapshotItem;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Baselines\BaselineCaptureService;
use App\Services\Baselines\BaselineContentCapturePhase;
use App\Services\Baselines\BaselineSnapshotIdentity;
use App\Services\Baselines\BaselineSnapshotItemNormalizer;
use App\Services\Baselines\CurrentStateHashResolver;
use App\Services\Baselines\Evidence\ResolvedEvidence;
use App\Services\Baselines\InventoryMetaContract;
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineFullContentRolloutGate;
use App\Support\Baselines\BaselineProfileStatus;
use App\Support\Baselines\BaselineReasonCodes;
use App\Support\Baselines\BaselineScope;
use App\Support\Baselines\BaselineSnapshotLifecycleState;
use App\Support\Baselines\PolicyVersionCapturePurpose;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
use Throwable;

class CaptureBaselineSnapshotJob implements ShouldQueue
{
    use BridgesFailedOperationRun;
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public int $timeout = 300;

    public bool $failOnTimeout = true;

    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 $identity,
        InventoryMetaContract $metaContract,
        AuditLogger $auditLogger,
        OperationRunService $operationRunService,
        mixed $arg5 = null,
        mixed $arg6 = null,
        ?BaselineSnapshotItemNormalizer $snapshotItemNormalizer = null,
        ?BaselineFullContentRolloutGate $rolloutGate = null,
    ): void {
        $captureService = $arg5 instanceof BaselineCaptureService
            ? $arg5
            : app(BaselineCaptureService::class);
        $hashResolver = $arg5 instanceof CurrentStateHashResolver
            ? $arg5
            : ($arg6 instanceof CurrentStateHashResolver
                ? $arg6
                : app(CurrentStateHashResolver::class));
        $contentCapturePhase = $arg5 instanceof BaselineContentCapturePhase
            ? $arg5
            : ($arg6 instanceof BaselineContentCapturePhase
                ? $arg6
                : app(BaselineContentCapturePhase::class));
        $snapshotItemNormalizer ??= app(BaselineSnapshotItemNormalizer::class);
        $rolloutGate ??= app(BaselineFullContentRolloutGate::class);

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

            return;
        }

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

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

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

        $sourceTenant = ManagedEnvironment::query()->find($sourceEnvironmentId);

        if (! $sourceTenant instanceof ManagedEnvironment) {
            throw new RuntimeException("Source ManagedEnvironment #{$sourceEnvironmentId} not found.");
        }

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

        $effectiveScope = BaselineScope::fromJsonb($context['effective_scope'] ?? null);
        $truthfulTypes = $this->truthfulTypesFromContext($context, $effectiveScope);

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

        if ($captureMode === BaselineCaptureMode::FullContent) {
            $rolloutGate->assertEnabled();
        }

        $previousCurrentSnapshot = $profile->resolveCurrentConsumableSnapshot();
        $previousCurrentSnapshotId = $previousCurrentSnapshot instanceof BaselineSnapshot
            ? (int) $previousCurrentSnapshot->getKey()
            : null;
        $previousCurrentSnapshotExists = $previousCurrentSnapshotId !== null;

        $preflightEligibility = is_array(data_get($context, 'baseline_capture.eligibility'))
            ? data_get($context, 'baseline_capture.eligibility')
            : [];
        $inventoryEligibility = $captureService->latestInventoryEligibilityDecision($sourceTenant, $effectiveScope, $truthfulTypes);
        $latestInventorySyncRunId = is_numeric($inventoryEligibility['inventory_sync_run_id'] ?? null)
            ? (int) $inventoryEligibility['inventory_sync_run_id']
            : null;
        $eligibilityContext = $captureService->eligibilityContextPayload($inventoryEligibility, phase: 'runtime_recheck');
        $eligibilityContext['changed_after_enqueue'] = ($preflightEligibility['ok'] ?? null) === true
            && ! ($inventoryEligibility['ok'] ?? false);
        $eligibilityContext['preflight_inventory_sync_run_id'] = is_numeric($preflightEligibility['inventory_sync_run_id'] ?? null)
            ? (int) $preflightEligibility['inventory_sync_run_id']
            : null;
        $eligibilityContext['preflight_reason_code'] = is_string($preflightEligibility['reason_code'] ?? null)
            ? (string) $preflightEligibility['reason_code']
            : null;

        $context['baseline_capture'] = array_merge(
            is_array($context['baseline_capture'] ?? null) ? $context['baseline_capture'] : [],
            [
                'inventory_sync_run_id' => $latestInventorySyncRunId,
                'eligibility' => $eligibilityContext,
                'previous_current_snapshot_id' => $previousCurrentSnapshotId,
                'previous_current_snapshot_exists' => $previousCurrentSnapshotExists,
            ],
        );
        $context['progress'] = array_merge(
            is_array($context['progress'] ?? null) ? $context['progress'] : [],
            [
                'phase' => [
                    'key' => 'preparing',
                    'label' => 'Preparing baseline capture.',
                ],
            ],
        );
        $this->operationRun->update(['context' => $context]);
        $this->operationRun->refresh();
        $context = is_array($this->operationRun->context) ? $this->operationRun->context : [];

        if (! ($inventoryEligibility['ok'] ?? false)) {
            $reasonCode = is_string($inventoryEligibility['reason_code'] ?? null)
                ? (string) $inventoryEligibility['reason_code']
                : BaselineReasonCodes::CAPTURE_INVENTORY_MISSING;
            $summaryCounts = [
                'total' => 0,
                'processed' => 0,
                'succeeded' => 0,
                'failed' => 0,
            ];
            $blockedContext = $context;
            $blockedContext['reason_code'] = $reasonCode;
            $blockedContext['baseline_capture'] = array_merge(
                is_array($blockedContext['baseline_capture'] ?? null) ? $blockedContext['baseline_capture'] : [],
                [
                    'reason_code' => $reasonCode,
                    'subjects_total' => 0,
                    'current_baseline_changed' => false,
                ],
            );
            $blockedContext['result'] = array_merge(
                is_array($blockedContext['result'] ?? null) ? $blockedContext['result'] : [],
                [
                    'current_baseline_changed' => false,
                ],
            );

            $this->operationRun->update([
                'context' => $blockedContext,
                'summary_counts' => $summaryCounts,
            ]);
            $this->operationRun->refresh();

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

            $operationRunService->finalizeBlockedRun(
                run: $this->operationRun,
                reasonCode: $reasonCode,
                message: $this->blockedInventoryMessage(
                    $reasonCode,
                    (bool) ($eligibilityContext['changed_after_enqueue'] ?? false),
                ),
            );

            $this->operationRun->refresh();

            $this->auditCompleted(
                auditLogger: $auditLogger,
                tenant: $sourceTenant,
                profile: $profile,
                snapshot: null,
                initiator: $initiator,
                captureMode: $captureMode,
                subjectsTotal: 0,
                inventorySyncRunId: $latestInventorySyncRunId,
                wasNewSnapshot: false,
                evidenceCaptureStats: [
                    'requested' => 0,
                    'succeeded' => 0,
                    'skipped' => 0,
                    'failed' => 0,
                    'throttled' => 0,
                ],
                gaps: [
                    'count' => 0,
                    'by_reason' => [],
                ],
                currentBaselineChanged: false,
                reasonCode: $reasonCode,
            );

            return;
        }

        $inventoryResult = $this->collectInventorySubjects(
            sourceTenant: $sourceTenant,
            scope: $effectiveScope,
            identity: $identity,
            latestInventorySyncRunId: $latestInventorySyncRunId,
            policyTypes: $truthfulTypes,
        );

        $subjects = $inventoryResult['subjects'];
        $inventoryByKey = $inventoryResult['inventory_by_key'];
        $subjectsTotal = $inventoryResult['subjects_total'];
        $captureGaps = $inventoryResult['gaps'];

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

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

        if ($captureMode === BaselineCaptureMode::FullContent) {
            $processingContext = is_array($this->operationRun->context) ? $this->operationRun->context : [];
            $processingContext['progress'] = array_merge(
                is_array($processingContext['progress'] ?? null) ? $processingContext['progress'] : [],
                [
                    'phase' => [
                        'key' => 'processing',
                        'label' => 'Capturing evidence.',
                    ],
                ],
            );
            $this->operationRun->update(['context' => $processingContext]);
            $this->operationRun->refresh();

            $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_capture'] ?? null)) {
                $resumeTokenIn = $context['baseline_capture']['resume_token'] ?? null;
            }

            $phaseResult = $contentCapturePhase->capture(
                tenant: $sourceTenant,
                subjects: $subjects,
                purpose: PolicyVersionCapturePurpose::BaselineCapture,
                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;
        }

        $resolvedEvidence = $hashResolver->resolveForSubjects(
            tenant: $sourceTenant,
            subjects: $subjects,
            since: null,
            latestInventorySyncRunId: $latestInventorySyncRunId,
        );

        $snapshotItems = $this->buildSnapshotItems(
            inventoryByKey: $inventoryByKey,
            resolvedEvidence: $resolvedEvidence,
            captureMode: $captureMode,
            gaps: $captureGaps,
        );

        $normalizedItems = $snapshotItemNormalizer->deduplicate($snapshotItems['items'] ?? []);
        $items = $normalizedItems['items'];

        if (($normalizedItems['duplicates'] ?? 0) > 0) {
            $captureGaps['duplicate_subject_reference'] = ($captureGaps['duplicate_subject_reference'] ?? 0) + (int) $normalizedItems['duplicates'];
        }

        $identityHash = $identity->computeIdentity($items);

        $gapsByReason = $this->mergeGapCounts($captureGaps, $phaseGaps);
        $gapsCount = array_sum($gapsByReason);

        $snapshotSummary = [
            'total_items' => count($items),
            'policy_type_counts' => $this->countByPolicyType($items),
            'fidelity_counts' => $snapshotItems['fidelity_counts'] ?? ['content' => 0, 'meta' => 0],
            'gaps' => [
                'count' => $gapsCount,
                'by_reason' => $gapsByReason,
            ],
        ];

        $persistingContext = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $persistingContext['progress'] = array_merge(
            is_array($persistingContext['progress'] ?? null) ? $persistingContext['progress'] : [],
            [
                'phase' => [
                    'key' => 'persisting',
                    'label' => 'Saving baseline snapshot.',
                ],
            ],
        );
        $this->operationRun->update(['context' => $persistingContext]);
        $this->operationRun->refresh();

        if ($subjectsTotal === 0) {
            $snapshotResult = $this->captureNoDataSnapshotArtifact(
                $profile,
                $identityHash,
                $snapshotSummary,
            );
            $snapshot = $snapshotResult['snapshot'];
            $wasNewSnapshot = $snapshotResult['was_new_snapshot'];
            $summaryCounts = [
                'total' => 0,
                'processed' => 0,
                'succeeded' => 0,
                'failed' => 0,
            ];
            $updatedContext = is_array($this->operationRun->context) ? $this->operationRun->context : [];
            $updatedContext['reason_code'] = BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS;
            $updatedContext['baseline_capture'] = array_merge(
                is_array($updatedContext['baseline_capture'] ?? null) ? $updatedContext['baseline_capture'] : [],
                [
                    'reason_code' => BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS,
                    'subjects_total' => 0,
                    'inventory_sync_run_id' => $latestInventorySyncRunId,
                    'evidence_capture' => $phaseStats,
                    'gaps' => [
                        'count' => $gapsCount,
                        'by_reason' => $gapsByReason,
                        'subjects' => is_array($phaseResult['gap_subjects'] ?? null) && $phaseResult['gap_subjects'] !== []
                            ? array_values($phaseResult['gap_subjects'])
                            : null,
                    ],
                    'resume_token' => $resumeToken,
                    'current_baseline_changed' => false,
                    'previous_current_snapshot_id' => $previousCurrentSnapshotId,
                    'previous_current_snapshot_exists' => $previousCurrentSnapshotExists,
                ],
            );
            $updatedContext['result'] = array_merge(
                is_array($updatedContext['result'] ?? null) ? $updatedContext['result'] : [],
                [
                    'snapshot_id' => (int) $snapshot->getKey(),
                    'snapshot_identity_hash' => $identityHash,
                    'was_new_snapshot' => $wasNewSnapshot,
                    'items_captured' => 0,
                    'snapshot_lifecycle' => $snapshot->lifecycleState()->value,
                    'current_baseline_changed' => false,
                ],
            );
            $updatedContext['progress'] = array_merge(
                is_array($updatedContext['progress'] ?? null) ? $updatedContext['progress'] : [],
                [
                    'phase' => [
                        'key' => 'finalizing',
                        'label' => 'Finalizing baseline capture.',
                    ],
                ],
            );

            $this->operationRun->update([
                'context' => $updatedContext,
                'summary_counts' => $summaryCounts,
            ]);
            $this->operationRun->refresh();

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

            $this->operationRun->refresh();

            $this->auditCompleted(
                auditLogger: $auditLogger,
                tenant: $sourceTenant,
                profile: $profile,
                snapshot: $snapshot,
                initiator: $initiator,
                captureMode: $captureMode,
                subjectsTotal: 0,
                inventorySyncRunId: $latestInventorySyncRunId,
                wasNewSnapshot: $wasNewSnapshot,
                evidenceCaptureStats: $phaseStats,
                gaps: [
                    'count' => $gapsCount,
                    'by_reason' => $gapsByReason,
                ],
                currentBaselineChanged: false,
                reasonCode: BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS,
            );

            return;
        }

        $snapshotResult = $this->captureSnapshotArtifact(
            $profile,
            $identityHash,
            $items,
            $snapshotSummary,
        );

        $snapshot = $snapshotResult['snapshot'];
        $wasNewSnapshot = $snapshotResult['was_new_snapshot'];

        if ($profile->status === BaselineProfileStatus::Active && $snapshot->isConsumable()) {
            $profile->update(['active_snapshot_id' => $snapshot->getKey()]);
        }

        $profile->refresh();
        $currentBaselineChanged = $this->currentBaselineChanged($profile, $previousCurrentSnapshotId);

        $warningsRecorded = $gapsByReason !== [] || $resumeToken !== null;
        $warningsRecorded = $warningsRecorded || ($captureMode === BaselineCaptureMode::FullContent && ($snapshotItems['fidelity_counts']['meta'] ?? 0) > 0);
        $outcome = $warningsRecorded ? OperationRunOutcome::PartiallySucceeded->value : OperationRunOutcome::Succeeded->value;

        $summaryCounts = [
            'total' => $subjectsTotal,
            'processed' => $subjectsTotal,
            'succeeded' => $snapshotItems['items_count'],
            'failed' => max(0, $subjectsTotal - $snapshotItems['items_count']),
        ];

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

        $updatedContext = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $updatedContext['baseline_capture'] = array_merge(
            is_array($updatedContext['baseline_capture'] ?? null) ? $updatedContext['baseline_capture'] : [],
            [
                'subjects_total' => $subjectsTotal,
                'inventory_sync_run_id' => $latestInventorySyncRunId,
                'evidence_capture' => $phaseStats,
                'gaps' => [
                    'count' => $gapsCount,
                    'by_reason' => $gapsByReason,
                    'subjects' => is_array($phaseResult['gap_subjects'] ?? null) && $phaseResult['gap_subjects'] !== []
                        ? array_values($phaseResult['gap_subjects'])
                        : null,
                ],
                'resume_token' => $resumeToken,
                'current_baseline_changed' => $currentBaselineChanged,
                'previous_current_snapshot_id' => $previousCurrentSnapshotId,
                'previous_current_snapshot_exists' => $previousCurrentSnapshotExists,
            ],
        );
        $updatedContext['result'] = [
            'snapshot_id' => (int) $snapshot->getKey(),
            'snapshot_identity_hash' => $identityHash,
            'was_new_snapshot' => $wasNewSnapshot,
            'items_captured' => $snapshotItems['items_count'],
            'snapshot_lifecycle' => $snapshot->lifecycleState()->value,
            'current_baseline_changed' => $currentBaselineChanged,
        ];
        $updatedContext['progress'] = array_merge(
            is_array($updatedContext['progress'] ?? null) ? $updatedContext['progress'] : [],
            [
                'phase' => [
                    'key' => 'finalizing',
                    'label' => 'Finalizing baseline capture.',
                ],
            ],
        );
        $this->operationRun->update(['context' => $updatedContext]);

        $this->auditCompleted(
            auditLogger: $auditLogger,
            tenant: $sourceTenant,
            profile: $profile,
            snapshot: $snapshot,
            initiator: $initiator,
            captureMode: $captureMode,
            subjectsTotal: $subjectsTotal,
            inventorySyncRunId: $latestInventorySyncRunId,
            wasNewSnapshot: $wasNewSnapshot,
            evidenceCaptureStats: $phaseStats,
            gaps: [
                'count' => $gapsCount,
                'by_reason' => $gapsByReason,
            ],
            currentBaselineChanged: $currentBaselineChanged,
            reasonCode: null,
        );
    }

    /**
     * @return array{
     *   subjects_total: int,
     *   subjects: list<array{policy_type: string, subject_external_id: string, subject_key: string}>,
     *   inventory_by_key: array<string, array{
     *     tenant_subject_external_id: string,
     *     workspace_subject_external_id: string,
     *     subject_key: string,
     *     policy_type: string,
     *     identity_strategy: string,
     *     display_name: ?string,
     *     category: ?string,
     *     platform: ?string,
     *     is_built_in: ?bool,
     *     role_permission_count: ?int
     *   }>,
     *   gaps: array<string, int>
     * }
     */
    private function collectInventorySubjects(
        ManagedEnvironment $sourceTenant,
        BaselineScope $scope,
        BaselineSnapshotIdentity $identity,
        ?int $latestInventorySyncRunId = null,
        ?array $policyTypes = null,
    ): array {
        $query = InventoryItem::query()
            ->where('managed_environment_id', $sourceTenant->getKey());

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

        $query->whereIn('policy_type', is_array($policyTypes) && $policyTypes !== [] ? $policyTypes : $scope->allTypes());

        /** @var array<string, array{tenant_subject_external_id: string, workspace_subject_external_id: string, subject_key: string, policy_type: string, identity_strategy: string, display_name: ?string, category: ?string, platform: ?string, is_built_in: ?bool, role_permission_count: ?int}> $inventoryByKey */
        $inventoryByKey = [];

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

        /**
         * Ensure we only include unambiguous subjects when matching by subject_key (derived from display name).
         *
         * When multiple inventory items share the same "policy_type|subject_key" we cannot reliably map them
         * across tenants, so we treat them as an evidence gap and exclude them from the snapshot.
         *
         * @var array<string, true> $ambiguousKeys
         */
        $ambiguousKeys = [];

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

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

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

                        continue;
                    }

                    $logicalKey = $policyType.'|'.$subjectKey;

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

                    if (array_key_exists($logicalKey, $subjectKeyToInventoryKey)) {
                        $ambiguousKeys[$logicalKey] = true;

                        $previousKey = $subjectKeyToInventoryKey[$logicalKey];
                        unset($subjectKeyToInventoryKey[$logicalKey], $inventoryByKey[$previousKey]);

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

                        continue;
                    }

                    $workspaceSafeId = $identity->workspaceSafeSubjectExternalId($policyType, $displayName, $tenantSubjectExternalId);

                    if (! is_string($workspaceSafeId) || $workspaceSafeId === '') {
                        $gaps['missing_subject_external_reference'] = ($gaps['missing_subject_external_reference'] ?? 0) + 1;

                        continue;
                    }

                    $key = $policyType.'|'.(string) $inventoryItem->external_id;
                    $subjectKeyToInventoryKey[$logicalKey] = $key;

                    $inventoryByKey[$key] = [
                        'tenant_subject_external_id' => (string) $inventoryItem->external_id,
                        'workspace_subject_external_id' => $workspaceSafeId,
                        'subject_key' => $subjectKey,
                        'policy_type' => $policyType,
                        'identity_strategy' => InventoryPolicyTypeMeta::baselineCompareIdentityStrategy($policyType),
                        'display_name' => $displayName,
                        'category' => is_string($inventoryItem->category) ? $inventoryItem->category : null,
                        'platform' => is_string($inventoryItem->platform) ? $inventoryItem->platform : null,
                        'is_built_in' => is_bool($metaJsonb['is_built_in'] ?? null) ? (bool) $metaJsonb['is_built_in'] : null,
                        'role_permission_count' => is_numeric($metaJsonb['role_permission_count'] ?? null) ? (int) $metaJsonb['role_permission_count'] : null,
                    ];
                }
            });

        ksort($gaps);

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

        return [
            'subjects_total' => count($subjects),
            'subjects' => $subjects,
            'inventory_by_key' => $inventoryByKey,
            'gaps' => $gaps,
        ];
    }

    /**
     * @param  array<string, mixed>  $context
     * @return list<string>
     */
    private function truthfulTypesFromContext(array $context, BaselineScope $effectiveScope): array
    {
        $truthfulTypes = data_get($context, 'effective_scope.truthful_types');

        if (is_array($truthfulTypes)) {
            $truthfulTypes = array_values(array_filter($truthfulTypes, 'is_string'));

            if ($truthfulTypes !== []) {
                sort($truthfulTypes, SORT_STRING);

                return $truthfulTypes;
            }
        }

        return $effectiveScope->allTypes();
    }

    /**
     * @param  array<string, array{
     *   tenant_subject_external_id: string,
     *   workspace_subject_external_id: string,
     *   subject_key: string,
     *   policy_type: string,
     *   identity_strategy: string,
     *   display_name: ?string,
     *   category: ?string,
     *   platform: ?string,
     *   is_built_in: ?bool,
     *   role_permission_count: ?int,
     * }>  $inventoryByKey
     * @param  array<string, ResolvedEvidence|null>  $resolvedEvidence
     * @param  array<string, int>  $gaps
     * @return array{
     *   items: array<int, array{
     *     subject_type: string,
     *     subject_external_id: string,
     *     subject_key: string,
     *     policy_type: string,
     *     baseline_hash: string,
     *     meta_jsonb: array<string, mixed>
     *   }>,
     *   items_count: int,
     *   fidelity_counts: array{content: int, meta: int}
     * }
     */
    private function buildSnapshotItems(
        array $inventoryByKey,
        array $resolvedEvidence,
        BaselineCaptureMode $captureMode,
        array &$gaps,
    ): array {
        $items = [];
        $fidelityCounts = ['content' => 0, 'meta' => 0];

        foreach ($inventoryByKey as $key => $inventoryItem) {
            $evidence = $resolvedEvidence[$key] ?? null;

            if (! $evidence instanceof ResolvedEvidence) {
                $gaps['missing_evidence'] = ($gaps['missing_evidence'] ?? 0) + 1;

                continue;
            }

            if ((string) $inventoryItem['policy_type'] === 'intuneRoleDefinition' && ! is_numeric($evidence->meta['policy_version_id'] ?? null)) {
                $gaps['missing_role_definition_version_reference'] = ($gaps['missing_role_definition_version_reference'] ?? 0) + 1;

                continue;
            }

            $provenance = $evidence->provenance();
            unset($provenance['observed_operation_run_id']);

            $fidelity = (string) ($provenance['fidelity'] ?? 'meta');
            $fidelityCounts[$fidelity === 'content' ? 'content' : 'meta']++;

            if ($captureMode === BaselineCaptureMode::FullContent && $fidelity !== 'content') {
                $gaps['meta_fallback'] = ($gaps['meta_fallback'] ?? 0) + 1;
            }

            $items[] = [
                'subject_type' => 'policy',
                'subject_external_id' => (string) $inventoryItem['workspace_subject_external_id'],
                'subject_key' => (string) $inventoryItem['subject_key'],
                'policy_type' => (string) $inventoryItem['policy_type'],
                'baseline_hash' => $evidence->hash,
                'meta_jsonb' => [
                    'display_name' => $inventoryItem['display_name'],
                    'category' => $inventoryItem['category'],
                    'platform' => $inventoryItem['platform'],
                    'evidence' => $provenance,
                    'identity' => [
                        'strategy' => $inventoryItem['identity_strategy'],
                        'subject_key' => $inventoryItem['subject_key'],
                        'workspace_subject_external_id' => $inventoryItem['workspace_subject_external_id'],
                    ],
                    'version_reference' => [
                        'policy_version_id' => is_numeric($evidence->meta['policy_version_id'] ?? null) ? (int) $evidence->meta['policy_version_id'] : null,
                        'capture_purpose' => is_string($evidence->meta['capture_purpose'] ?? null) ? (string) $evidence->meta['capture_purpose'] : null,
                    ],
                    'rbac' => [
                        'is_built_in' => $inventoryItem['is_built_in'],
                        'role_permission_count' => $inventoryItem['role_permission_count'],
                    ],
                ],
            ];
        }

        return [
            'items' => $items,
            'items_count' => count($items),
            'fidelity_counts' => $fidelityCounts,
        ];
    }

    /**
     * @param  array<int, array<string, mixed>>  $snapshotItems
     * @param  array<string, mixed>  $summaryJsonb
     * @return array{snapshot: BaselineSnapshot, was_new_snapshot: bool}
     */
    private function captureSnapshotArtifact(
        BaselineProfile $profile,
        string $identityHash,
        array $snapshotItems,
        array $summaryJsonb,
    ): array {
        $existing = $this->findExistingConsumableSnapshot($profile, $identityHash);

        if ($existing instanceof BaselineSnapshot) {
            $this->rememberSnapshotOnRun(
                snapshot: $existing,
                identityHash: $identityHash,
                wasNewSnapshot: false,
                expectedItems: count($snapshotItems),
                persistedItems: count($snapshotItems),
            );

            return [
                'snapshot' => $existing,
                'was_new_snapshot' => false,
            ];
        }

        $expectedItems = count($snapshotItems);
        $snapshot = $this->createBuildingSnapshot($profile, $identityHash, $summaryJsonb, $expectedItems);

        $this->rememberSnapshotOnRun(
            snapshot: $snapshot,
            identityHash: $identityHash,
            wasNewSnapshot: true,
            expectedItems: $expectedItems,
            persistedItems: 0,
        );

        try {
            $persistedItems = $this->persistSnapshotItems($snapshot, $snapshotItems);

            if ($persistedItems !== $expectedItems) {
                throw new RuntimeException('Baseline snapshot completion proof failed.');
            }

            $snapshot->markComplete($identityHash, [
                'expected_identity_hash' => $identityHash,
                'expected_items' => $expectedItems,
                'persisted_items' => $persistedItems,
                'producer_run_id' => (int) $this->operationRun->getKey(),
                'was_empty_capture' => $expectedItems === 0,
            ]);

            $snapshot->refresh();

            $this->rememberSnapshotOnRun(
                snapshot: $snapshot,
                identityHash: $identityHash,
                wasNewSnapshot: true,
                expectedItems: $expectedItems,
                persistedItems: $persistedItems,
            );

            return [
                'snapshot' => $snapshot,
                'was_new_snapshot' => true,
            ];
        } catch (Throwable $exception) {
            $persistedItems = (int) BaselineSnapshotItem::query()
                ->where('baseline_snapshot_id', (int) $snapshot->getKey())
                ->count();

            $reasonCode = $exception instanceof RuntimeException
                && $exception->getMessage() === 'Baseline snapshot completion proof failed.'
                    ? BaselineReasonCodes::SNAPSHOT_COMPLETION_PROOF_FAILED
                    : BaselineReasonCodes::SNAPSHOT_CAPTURE_FAILED;

            $snapshot->markIncomplete($reasonCode, [
                'expected_identity_hash' => $identityHash,
                'expected_items' => $expectedItems,
                'persisted_items' => $persistedItems,
                'producer_run_id' => (int) $this->operationRun->getKey(),
                'was_empty_capture' => $expectedItems === 0,
            ]);

            $snapshot->refresh();

            $this->rememberSnapshotOnRun(
                snapshot: $snapshot,
                identityHash: $identityHash,
                wasNewSnapshot: true,
                expectedItems: $expectedItems,
                persistedItems: $persistedItems,
                reasonCode: $reasonCode,
            );

            throw $exception;
        }
    }

    /**
     * @param  array<string, mixed>  $summaryJsonb
     * @return array{snapshot: BaselineSnapshot, was_new_snapshot: bool}
     */
    private function captureNoDataSnapshotArtifact(
        BaselineProfile $profile,
        string $identityHash,
        array $summaryJsonb,
    ): array {
        $snapshot = $this->createBuildingSnapshot($profile, $identityHash, $summaryJsonb, 0);

        $this->rememberSnapshotOnRun(
            snapshot: $snapshot,
            identityHash: $identityHash,
            wasNewSnapshot: true,
            expectedItems: 0,
            persistedItems: 0,
            reasonCode: BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS,
        );

        $snapshot->markIncomplete(BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS, [
            'expected_identity_hash' => $identityHash,
            'expected_items' => 0,
            'persisted_items' => 0,
            'producer_run_id' => (int) $this->operationRun->getKey(),
            'was_empty_capture' => true,
        ]);

        $snapshot->refresh();

        $this->rememberSnapshotOnRun(
            snapshot: $snapshot,
            identityHash: $identityHash,
            wasNewSnapshot: true,
            expectedItems: 0,
            persistedItems: 0,
            reasonCode: BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS,
        );

        return [
            'snapshot' => $snapshot,
            'was_new_snapshot' => true,
        ];
    }

    private function findExistingConsumableSnapshot(BaselineProfile $profile, string $identityHash): ?BaselineSnapshot
    {
        $existing = BaselineSnapshot::query()
            ->where('workspace_id', $profile->workspace_id)
            ->where('baseline_profile_id', $profile->getKey())
            ->where('snapshot_identity_hash', $identityHash)
            ->where('lifecycle_state', BaselineSnapshotLifecycleState::Complete->value)
            ->first();

        return $existing instanceof BaselineSnapshot ? $existing : null;
    }

    /**
     * @param  array<string, mixed>  $summaryJsonb
     */
    private function createBuildingSnapshot(
        BaselineProfile $profile,
        string $identityHash,
        array $summaryJsonb,
        int $expectedItems,
    ): BaselineSnapshot {
        return BaselineSnapshot::create([
            'workspace_id' => (int) $profile->workspace_id,
            'baseline_profile_id' => (int) $profile->getKey(),
            'snapshot_identity_hash' => $this->temporarySnapshotIdentityHash($profile),
            'captured_at' => now(),
            'lifecycle_state' => BaselineSnapshotLifecycleState::Building->value,
            'summary_jsonb' => $summaryJsonb,
            'completion_meta_jsonb' => [
                'expected_identity_hash' => $identityHash,
                'expected_items' => $expectedItems,
                'persisted_items' => 0,
                'producer_run_id' => (int) $this->operationRun->getKey(),
                'was_empty_capture' => $expectedItems === 0,
            ],
        ]);
    }

    /**
     * @param  array<int, array<string, mixed>>  $snapshotItems
     */
    private function persistSnapshotItems(BaselineSnapshot $snapshot, array $snapshotItems): int
    {
        $persistedItems = 0;

        foreach (array_chunk($snapshotItems, 100) as $chunk) {
            $rows = array_map(
                fn (array $item): array => [
                    'baseline_snapshot_id' => (int) $snapshot->getKey(),
                    'subject_type' => $item['subject_type'],
                    'subject_external_id' => $item['subject_external_id'],
                    'subject_key' => $item['subject_key'],
                    'policy_type' => $item['policy_type'],
                    'baseline_hash' => $item['baseline_hash'],
                    'meta_jsonb' => json_encode($item['meta_jsonb']),
                    'created_at' => now(),
                    'updated_at' => now(),
                ],
                $chunk,
            );

            BaselineSnapshotItem::insert($rows);
            $persistedItems += count($rows);
        }

        return $persistedItems;
    }

    private function temporarySnapshotIdentityHash(BaselineProfile $profile): string
    {
        return hash(
            'sha256',
            implode('|', [
                'building',
                (string) $profile->getKey(),
                (string) $this->operationRun->getKey(),
                (string) microtime(true),
            ]),
        );
    }

    private function rememberSnapshotOnRun(
        BaselineSnapshot $snapshot,
        string $identityHash,
        bool $wasNewSnapshot,
        int $expectedItems,
        int $persistedItems,
        ?string $reasonCode = null,
    ): void {
        $context = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $context['baseline_snapshot_id'] = (int) $snapshot->getKey();
        $context['result'] = array_merge(
            is_array($context['result'] ?? null) ? $context['result'] : [],
            [
                'snapshot_id' => (int) $snapshot->getKey(),
                'snapshot_identity_hash' => $identityHash,
                'was_new_snapshot' => $wasNewSnapshot,
                'items_captured' => $persistedItems,
                'snapshot_lifecycle' => $snapshot->lifecycleState()->value,
                'expected_items' => $expectedItems,
            ],
        );

        if (is_string($reasonCode) && $reasonCode !== '') {
            $context['reason_code'] = $reasonCode;
            $context['result']['snapshot_reason_code'] = $reasonCode;
        } else {
            unset($context['reason_code'], $context['result']['snapshot_reason_code']);
        }

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

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

        foreach ($items as $item) {
            $type = (string) $item['policy_type'];
            $counts[$type] = ($counts[$type] ?? 0) + 1;
        }

        ksort($counts);

        return $counts;
    }

    private function currentBaselineChanged(BaselineProfile $profile, ?int $previousCurrentSnapshotId): bool
    {
        $currentSnapshot = $profile->resolveCurrentConsumableSnapshot();
        $currentSnapshotId = $currentSnapshot instanceof BaselineSnapshot
            ? (int) $currentSnapshot->getKey()
            : null;

        return $currentSnapshotId !== null && $currentSnapshotId !== $previousCurrentSnapshotId;
    }

    private function blockedInventoryMessage(string $reasonCode, bool $changedAfterEnqueue): string
    {
        return match ($reasonCode) {
            BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED => $changedAfterEnqueue
                ? 'Capture blocked because the latest inventory sync changed after the run was queued.'
                : 'Capture blocked because the latest inventory sync was blocked.',
            BaselineReasonCodes::CAPTURE_INVENTORY_FAILED => $changedAfterEnqueue
                ? 'Capture blocked because the latest inventory sync failed after the run was queued.'
                : 'Capture blocked because the latest inventory sync failed.',
            BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE => $changedAfterEnqueue
                ? 'Capture blocked because the latest inventory coverage became unusable after the run was queued.'
                : 'Capture blocked because the latest inventory coverage was not usable for this baseline scope.',
            default => 'Capture blocked because no credible inventory basis was available.',
        };
    }

    private function auditStarted(
        AuditLogger $auditLogger,
        ManagedEnvironment $tenant,
        BaselineProfile $profile,
        ?User $initiator,
        BaselineCaptureMode $captureMode,
        int $subjectsTotal,
        BaselineScope $effectiveScope,
        ?int $inventorySyncRunId,
    ): void {
        $auditLogger->log(
            tenant: $tenant,
            action: 'baseline.capture.started',
            context: [
                'metadata' => [
                    'operation_run_id' => (int) $this->operationRun->getKey(),
                    'baseline_profile_id' => (int) $profile->getKey(),
                    'baseline_profile_name' => (string) $profile->name,
                    'purpose' => PolicyVersionCapturePurpose::BaselineCapture->value,
                    'capture_mode' => $captureMode->value,
                    'inventory_sync_run_id' => $inventorySyncRunId,
                    '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,
        ManagedEnvironment $tenant,
        BaselineProfile $profile,
        ?BaselineSnapshot $snapshot,
        ?User $initiator,
        BaselineCaptureMode $captureMode,
        int $subjectsTotal,
        ?int $inventorySyncRunId,
        bool $wasNewSnapshot,
        array $evidenceCaptureStats,
        array $gaps,
        bool $currentBaselineChanged,
        ?string $reasonCode,
    ): void {
        $auditLogger->log(
            tenant: $tenant,
            action: 'baseline.capture.completed',
            context: [
                'metadata' => [
                    'operation_run_id' => (int) $this->operationRun->getKey(),
                    'baseline_profile_id' => (int) $profile->getKey(),
                    'baseline_profile_name' => (string) $profile->name,
                    'purpose' => PolicyVersionCapturePurpose::BaselineCapture->value,
                    'capture_mode' => $captureMode->value,
                    'inventory_sync_run_id' => $inventorySyncRunId,
                    'subjects_total' => $subjectsTotal,
                    'snapshot_id' => $snapshot?->getKey(),
                    'snapshot_identity_hash' => $snapshot instanceof BaselineSnapshot ? (string) $snapshot->snapshot_identity_hash : null,
                    'reason_code' => $reasonCode,
                    'current_baseline_changed' => $currentBaselineChanged,
                    'was_new_snapshot' => $wasNewSnapshot,
                    'evidence_capture' => $evidenceCaptureStats,
                    'gaps' => $gaps,
                ],
            ],
            actorId: $initiator?->id,
            actorEmail: $initiator?->email,
            actorName: $initiator?->name,
            resourceType: 'operation_run',
            resourceId: (string) $this->operationRun->getKey(),
        );
    }

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

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

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

        ksort($merged);

        return $merged;
    }
}
