<?php

declare(strict_types=1);

namespace App\Support\OpsUx;

use App\Models\OperationRun;
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeDomain;
use App\Support\Baselines\BaselineCompareReasonCode;
use App\Support\Baselines\BaselineReasonCodes;
use App\Support\OperationCatalog;
use App\Support\ReasonTranslation\ReasonPresenter;
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use App\Support\Ui\OperatorExplanation\CountDescriptor;
use App\Support\Ui\OperatorExplanation\OperatorExplanationPattern;

final class GovernanceRunDiagnosticSummaryBuilder
{
    public function __construct(
        private readonly ArtifactTruthPresenter $artifactTruthPresenter,
        private readonly ReasonPresenter $reasonPresenter,
    ) {}

    public function build(
        OperationRun $run,
        ?ArtifactTruthEnvelope $artifactTruth = null,
        ?OperatorExplanationPattern $operatorExplanation = null,
        ?ReasonResolutionEnvelope $reasonEnvelope = null,
    ): ?GovernanceRunDiagnosticSummary {
        if (! $run->supportsOperatorExplanation()) {
            return null;
        }

        $artifactTruth ??= $this->artifactTruthPresenter->forOperationRun($run);
        $operatorExplanation ??= $artifactTruth?->operatorExplanation;
        $reasonEnvelope ??= $this->reasonPresenter->forOperationRun($run, 'run_detail');

        if (! $artifactTruth instanceof ArtifactTruthEnvelope && ! $operatorExplanation instanceof OperatorExplanationPattern) {
            return null;
        }

        $canonicalType = OperationCatalog::canonicalCode((string) $run->type);
        $context = is_array($run->context) ? $run->context : [];
        $counts = SummaryCountsNormalizer::normalize(is_array($run->summary_counts) ? $run->summary_counts : []);
        $causeCandidates = $this->rankCauseCandidates($canonicalType, $run, $artifactTruth, $operatorExplanation, $reasonEnvelope, $context);
        $dominantCause = $causeCandidates[0] ?? $this->fallbackCause($artifactTruth, $operatorExplanation, $reasonEnvelope);
        $secondaryCauses = array_values(array_slice($causeCandidates, 1));
        $artifactImpactLabel = $this->artifactImpactLabel($artifactTruth, $operatorExplanation);
        $headline = $this->headline($canonicalType, $run, $artifactTruth, $operatorExplanation, $dominantCause, $context, $counts);
        $primaryReason = $this->primaryReason($dominantCause, $artifactTruth, $operatorExplanation, $reasonEnvelope);
        $nextActionCategory = $this->nextActionCategory($canonicalType, $run, $reasonEnvelope, $operatorExplanation, $context);
        $nextActionText = $this->nextActionText($artifactTruth, $operatorExplanation, $reasonEnvelope);
        $affectedScaleCue = $this->affectedScaleCue($canonicalType, $run, $artifactTruth, $operatorExplanation, $context, $counts);
        $secondaryFacts = $this->secondaryFacts($artifactTruth, $operatorExplanation, $secondaryCauses, $nextActionCategory, $nextActionText);

        return new GovernanceRunDiagnosticSummary(
            headline: $headline,
            executionOutcomeLabel: $this->executionOutcomeLabel($run),
            artifactImpactLabel: $artifactImpactLabel,
            primaryReason: $primaryReason,
            affectedScaleCue: $affectedScaleCue,
            nextActionCategory: $nextActionCategory,
            nextActionText: $nextActionText,
            dominantCause: [
                'code' => $dominantCause['code'] ?? null,
                'label' => $dominantCause['label'],
                'explanation' => $dominantCause['explanation'],
            ],
            secondaryCauses: array_map(
                static fn (array $cause): array => [
                    'code' => $cause['code'] ?? null,
                    'label' => $cause['label'],
                    'explanation' => $cause['explanation'],
                ],
                $secondaryCauses,
            ),
            secondaryFacts: $secondaryFacts,
            diagnosticsAvailable: (bool) ($operatorExplanation?->diagnosticsAvailable ?? false),
        );
    }

    private function executionOutcomeLabel(OperationRun $run): string
    {
        $spec = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, (string) $run->outcome);

        return $spec->label !== 'Unknown'
            ? $spec->label
            : ucfirst(str_replace('_', ' ', trim((string) $run->outcome)));
    }

    private function artifactImpactLabel(
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
    ): string {
        if ($artifactTruth instanceof ArtifactTruthEnvelope && trim($artifactTruth->primaryLabel) !== '') {
            return $artifactTruth->primaryLabel;
        }

        if ($operatorExplanation instanceof OperatorExplanationPattern) {
            return $operatorExplanation->trustworthinessLabel();
        }

        return 'Result needs review';
    }

    /**
     * @param  array{code: ?string, label: string, explanation: string}  $dominantCause
     * @param  array<string, mixed>  $context
     * @param  array<string, int>  $counts
     */
    private function headline(
        string $canonicalType,
        OperationRun $run,
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        array $dominantCause,
        array $context,
        array $counts,
    ): string {
        return match ($canonicalType) {
            'baseline.capture' => $this->baselineCaptureHeadline($artifactTruth, $context, $counts, $operatorExplanation),
            'baseline.compare' => $this->baselineCompareHeadline($artifactTruth, $context, $counts, $operatorExplanation),
            'tenant.evidence.snapshot.generate' => $this->evidenceSnapshotHeadline($artifactTruth, $operatorExplanation),
            'environment.review.compose' => $this->reviewComposeHeadline($artifactTruth, $dominantCause, $operatorExplanation),
            'environment.review_pack.generate' => $this->reviewPackHeadline($artifactTruth, $dominantCause, $operatorExplanation),
            default => $operatorExplanation?->headline
                ?? $artifactTruth?->primaryExplanation
                ?? 'This governance run needs review before it can be relied on.',
        };
    }

    /**
     * @param  array<string, mixed>  $context
     * @param  array<string, int>  $counts
     */
    private function baselineCaptureHeadline(
        ?ArtifactTruthEnvelope $artifactTruth,
        array $context,
        array $counts,
        ?OperatorExplanationPattern $operatorExplanation,
    ): string {
        $reasonCode = (string) data_get($context, 'baseline_capture.reason_code', data_get($context, 'reason_code', ''));
        $subjectsTotal = $this->intValue(data_get($context, 'baseline_capture.subjects_total'));
        $resumeToken = data_get($context, 'baseline_capture.resume_token');
        $gapCount = $this->intValue(data_get($context, 'baseline_capture.gaps.count'));
        $changedAfterEnqueue = data_get($context, 'baseline_capture.eligibility.changed_after_enqueue') === true;

        if ($reasonCode === BaselineReasonCodes::CAPTURE_INVENTORY_MISSING) {
            return 'The baseline capture could not continue because no current inventory basis was available.';
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED) {
            return $changedAfterEnqueue
                ? 'The baseline capture stopped because the latest inventory sync changed after the run was queued.'
                : 'The baseline capture was blocked because the latest inventory sync was blocked.';
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_INVENTORY_FAILED) {
            return $changedAfterEnqueue
                ? 'The baseline capture stopped because the latest inventory sync failed after the run was queued.'
                : 'The baseline capture was blocked because the latest inventory sync failed.';
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE) {
            return $changedAfterEnqueue
                ? 'The baseline capture stopped because the latest inventory coverage became unusable after the run was queued.'
                : 'The baseline capture could not produce a usable baseline because the latest inventory coverage was not credible.';
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS) {
            return 'The baseline capture finished without a usable baseline because no governed subjects were in scope.';
        }

        if ($subjectsTotal === 0) {
            return 'No baseline was captured because no governed subjects were ready.';
        }

        if (is_string($resumeToken) && trim($resumeToken) !== '') {
            return 'The baseline capture started, but more evidence still needs to be collected.';
        }

        if ($gapCount > 0) {
            return 'The baseline capture finished, but evidence gaps still limit the snapshot.';
        }

        if (($artifactTruth?->artifactExistence ?? null) === 'created_but_not_usable') {
            return 'The baseline capture finished without a usable snapshot.';
        }

        if (($counts['total'] ?? 0) === 0 && ($counts['processed'] ?? 0) === 0) {
            return 'The baseline capture finished without producing a decision-grade snapshot.';
        }

        return $operatorExplanation?->headline
            ?? $artifactTruth?->primaryExplanation
            ?? 'The baseline capture needs review before it can be used.';
    }

    /**
     * @param  array<string, mixed>  $context
     * @param  array<string, int>  $counts
     */
    private function baselineCompareHeadline(
        ?ArtifactTruthEnvelope $artifactTruth,
        array $context,
        array $counts,
        ?OperatorExplanationPattern $operatorExplanation,
    ): string {
        $reasonCode = (string) data_get($context, 'baseline_compare.reason_code', '');
        $proof = data_get($context, 'baseline_compare.coverage.proof');
        $resumeToken = data_get($context, 'baseline_compare.resume_token');

        if ($reasonCode === BaselineCompareReasonCode::AmbiguousSubjects->value) {
            return 'The compare finished, but ambiguous subject matching limited the result.';
        }

        if ($reasonCode === BaselineCompareReasonCode::StrategyFailed->value) {
            return 'The compare finished, but a compare strategy failure kept the result incomplete.';
        }

        if (is_string($resumeToken) && trim($resumeToken) !== '') {
            return 'The compare finished, but evidence capture still needs to resume before the result is complete.';
        }

        if (($counts['total'] ?? 0) === 0 && ($counts['processed'] ?? 0) === 0) {
            return 'The compare finished, but no decision-grade result is available yet.';
        }

        if ($proof === false) {
            return 'The compare finished, but missing coverage proof suppressed the normal result.';
        }

        return $operatorExplanation?->headline
            ?? $artifactTruth?->primaryExplanation
            ?? 'The compare needs follow-up before it can be treated as complete.';
    }

    private function evidenceSnapshotHeadline(
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
    ): string {
        return match (true) {
            $artifactTruth?->freshnessState === 'stale' => 'The snapshot finished processing, but its evidence basis is already stale.',
            $artifactTruth?->contentState === 'partial' => 'The snapshot finished processing, but its evidence basis is incomplete.',
            $artifactTruth?->contentState === 'missing_input' => 'The snapshot finished processing without a complete evidence basis.',
            default => $operatorExplanation?->headline
                ?? $artifactTruth?->primaryExplanation
                ?? 'The evidence snapshot needs review before it is relied on.',
        };
    }

    /**
     * @param  array{code: ?string, label: string, explanation: string}  $dominantCause
     */
    private function reviewComposeHeadline(
        ?ArtifactTruthEnvelope $artifactTruth,
        array $dominantCause,
        ?OperatorExplanationPattern $operatorExplanation,
    ): string {
        return match (true) {
            $artifactTruth?->contentState === 'partial' && $artifactTruth?->freshnessState === 'stale'
                => 'The review was generated, but missing sections and stale evidence keep it from being decision-grade.',
            $artifactTruth?->contentState === 'partial'
                => 'The review was generated, but required sections are still incomplete.',
            $artifactTruth?->freshnessState === 'stale'
                => 'The review was generated, but it relies on stale evidence.',
            default => $operatorExplanation?->headline
                ?? $dominantCause['explanation']
                ?? 'The review needs follow-up before it should guide action.',
        };
    }

    /**
     * @param  array{code: ?string, label: string, explanation: string}  $dominantCause
     */
    private function reviewPackHeadline(
        ?ArtifactTruthEnvelope $artifactTruth,
        array $dominantCause,
        ?OperatorExplanationPattern $operatorExplanation,
    ): string {
        return match (true) {
            $artifactTruth?->publicationReadiness === 'blocked'
                => 'The pack did not produce a shareable artifact yet.',
            $artifactTruth?->publicationReadiness === 'internal_only'
                => 'The pack finished, but it should stay internal until the source review is refreshed.',
            default => $operatorExplanation?->headline
                ?? $dominantCause['explanation']
                ?? 'The review pack needs follow-up before it is shared.',
        };
    }

    /**
     * @param  array{code: ?string, label: string, explanation: string}  $dominantCause
     */
    private function primaryReason(
        array $dominantCause,
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        ?ReasonResolutionEnvelope $reasonEnvelope,
    ): string {
        return $dominantCause['explanation']
            ?? $operatorExplanation?->dominantCauseExplanation
            ?? $reasonEnvelope?->shortExplanation
            ?? $artifactTruth?->primaryExplanation
            ?? $operatorExplanation?->reliabilityStatement
            ?? 'TenantPilot recorded diagnostic detail for this run.';
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private function nextActionCategory(
        string $canonicalType,
        OperationRun $run,
        ?ReasonResolutionEnvelope $reasonEnvelope,
        ?OperatorExplanationPattern $operatorExplanation,
        array $context,
    ): string {
        if ($reasonEnvelope?->actionability === 'retryable_transient' || $operatorExplanation?->nextActionCategory === 'retry_later') {
            return 'retry_later';
        }

        if (in_array($canonicalType, ['baseline.capture', 'baseline.compare'], true)) {
            $resumeToken = $canonicalType === 'baseline.capture'
                ? data_get($context, 'baseline_capture.resume_token')
                : data_get($context, 'baseline_compare.resume_token');

            if (is_string($resumeToken) && trim($resumeToken) !== '') {
                return 'resume_capture_or_generation';
            }
        }

        $reasonCode = (string) (data_get($context, 'baseline_compare.reason_code') ?? $reasonEnvelope?->internalCode ?? '');

        if (in_array($reasonCode, [
            BaselineCompareReasonCode::AmbiguousSubjects->value,
            BaselineCompareReasonCode::UnsupportedSubjects->value,
        ], true)) {
            return 'review_scope_or_ambiguous_matches';
        }

        if ($canonicalType === 'baseline.capture' && $this->intValue(data_get($context, 'baseline_capture.subjects_total')) === 0) {
            return 'refresh_prerequisite_data';
        }

        if ($operatorExplanation?->nextActionCategory === 'none' || trim((string) $operatorExplanation?->nextActionText) === 'No action needed') {
            return 'no_further_action';
        }

        if (
            $reasonEnvelope?->actionability === 'prerequisite_missing'
            || in_array($canonicalType, ['tenant.evidence.snapshot.generate', 'environment.review.compose', 'environment.review_pack.generate'], true)
        ) {
            return 'refresh_prerequisite_data';
        }

        return 'manually_validate';
    }

    private function nextActionText(
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        ?ReasonResolutionEnvelope $reasonEnvelope,
    ): string {
        $text = $operatorExplanation?->nextActionText
            ?? $artifactTruth?->nextStepText()
            ?? $reasonEnvelope?->firstNextStep()?->label
            ?? 'No action needed';

        return trim(rtrim($text, '.')).'.';
    }

    /**
     * @param  array<string, mixed>  $context
     * @param  array<string, int>  $counts
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function affectedScaleCue(
        string $canonicalType,
        OperationRun $run,
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        array $context,
        array $counts,
    ): ?array {
        return match ($canonicalType) {
            'baseline.capture' => $this->baselineCaptureScaleCue($context, $counts),
            'baseline.compare' => $this->baselineCompareScaleCue($context, $counts),
            'tenant.evidence.snapshot.generate' => $this->countDescriptorScaleCue($operatorExplanation?->countDescriptors ?? [], ['Missing dimensions', 'Stale dimensions', 'Evidence dimensions']),
            'environment.review.compose' => $this->reviewScaleCue($artifactTruth, $operatorExplanation?->countDescriptors ?? []),
            'environment.review_pack.generate' => $this->reviewPackScaleCue($artifactTruth, $operatorExplanation?->countDescriptors ?? []),
            default => $this->summaryCountsScaleCue($counts),
        };
    }

    /**
     * @param  array<string, mixed>  $context
     * @param  array<string, int>  $counts
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function baselineCaptureScaleCue(array $context, array $counts): ?array
    {
        $subjectsTotal = $this->intValue(data_get($context, 'baseline_capture.subjects_total'));
        $gapCount = $this->intValue(data_get($context, 'baseline_capture.gaps.count'));

        if ($gapCount > 0) {
            return [
                'label' => 'Affected subjects',
                'value' => "{$gapCount} governed subjects still need evidence follow-up.",
                'source' => 'context',
                'confidence' => 'exact',
            ];
        }

        if ($subjectsTotal >= 0) {
            return [
                'label' => 'Capture scope',
                'value' => "{$subjectsTotal} governed subjects were in the recorded capture scope.",
                'source' => 'context',
                'confidence' => 'exact',
            ];
        }

        return $this->summaryCountsScaleCue($counts);
    }

    /**
     * @param  array<string, mixed>  $context
     * @param  array<string, int>  $counts
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function baselineCompareScaleCue(array $context, array $counts): ?array
    {
        $gapCount = $this->intValue(data_get($context, 'baseline_compare.evidence_gaps.count'));
        $subjectsTotal = $this->intValue(data_get($context, 'baseline_compare.subjects_total'));
        $uncoveredTypes = array_values(array_filter((array) data_get($context, 'baseline_compare.coverage.uncovered_types', []), 'is_string'));

        if ($gapCount > 0) {
            return [
                'label' => 'Affected subjects',
                'value' => "{$gapCount} governed subjects still have evidence gaps.",
                'source' => 'context',
                'confidence' => 'exact',
            ];
        }

        if ($uncoveredTypes !== []) {
            $count = count($uncoveredTypes);

            return [
                'label' => 'Coverage scope',
                'value' => "{$count} policy types were left without proven compare coverage.",
                'source' => 'context',
                'confidence' => 'bounded',
            ];
        }

        if ($subjectsTotal > 0) {
            return [
                'label' => 'Compare scope',
                'value' => "{$subjectsTotal} governed subjects were in scope for this compare run.",
                'source' => 'context',
                'confidence' => 'exact',
            ];
        }

        return $this->summaryCountsScaleCue($counts);
    }

    /**
     * @param  array<int, CountDescriptor>  $countDescriptors
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function reviewScaleCue(?ArtifactTruthEnvelope $artifactTruth, array $countDescriptors): ?array
    {
        if ($artifactTruth?->contentState === 'partial') {
            $sections = $this->findCountDescriptor($countDescriptors, 'Sections');

            if ($sections instanceof CountDescriptor) {
                return [
                    'label' => 'Review sections',
                    'value' => "{$sections->value} sections were recorded and still need review for completeness.",
                    'source' => 'related_artifact_truth',
                    'confidence' => 'best_available',
                ];
            }

            return [
                'label' => 'Review sections',
                'value' => 'Required review sections are still incomplete.',
                'source' => 'related_artifact_truth',
                'confidence' => 'best_available',
            ];
        }

        if ($artifactTruth?->freshnessState === 'stale') {
            return [
                'label' => 'Evidence freshness',
                'value' => 'The source evidence is stale for at least part of this review.',
                'source' => 'related_artifact_truth',
                'confidence' => 'best_available',
            ];
        }

        return $this->countDescriptorScaleCue($countDescriptors, ['Sections', 'Findings']);
    }

    /**
     * @param  array<int, CountDescriptor>  $countDescriptors
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function reviewPackScaleCue(?ArtifactTruthEnvelope $artifactTruth, array $countDescriptors): ?array
    {
        if ($artifactTruth?->publicationReadiness === 'internal_only') {
            return [
                'label' => 'Sharing scope',
                'value' => 'The pack is suitable for internal follow-up only in its current state.',
                'source' => 'related_artifact_truth',
                'confidence' => 'best_available',
            ];
        }

        return $this->countDescriptorScaleCue($countDescriptors, ['Reports', 'Findings']);
    }

    /**
     * @param  array<int, CountDescriptor>  $countDescriptors
     * @param  list<string>  $preferredLabels
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function countDescriptorScaleCue(array $countDescriptors, array $preferredLabels): ?array
    {
        foreach ($preferredLabels as $label) {
            $descriptor = $this->findCountDescriptor($countDescriptors, $label);

            if (! $descriptor instanceof CountDescriptor || $descriptor->value <= 0) {
                continue;
            }

            return [
                'label' => $descriptor->label,
                'value' => "{$descriptor->value} {$this->pluralizeDescriptor($descriptor)}.",
                'source' => 'related_artifact_truth',
                'confidence' => 'exact',
            ];
        }

        return null;
    }

    /**
     * @param  array<string, int>  $counts
     * @return array{label: string, value: string, source: string, confidence?: string}|null
     */
    private function summaryCountsScaleCue(array $counts): ?array
    {
        foreach (['total', 'processed', 'failed', 'items', 'finding_count'] as $key) {
            $value = (int) ($counts[$key] ?? 0);

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

            return [
                'label' => SummaryCountsNormalizer::label($key),
                'value' => "{$value} recorded in the canonical run counters.",
                'source' => 'summary_counts',
                'confidence' => 'exact',
            ];
        }

        return null;
    }

    /**
     * @param  array<string, mixed>  $context
     * @return list<array{rank: int, code: ?string, label: string, explanation: string}>
     */
    private function rankCauseCandidates(
        string $canonicalType,
        OperationRun $run,
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        ?ReasonResolutionEnvelope $reasonEnvelope,
        array $context,
    ): array {
        $candidates = [];

        $this->pushCandidate(
            $candidates,
            code: $reasonEnvelope?->internalCode ?? $operatorExplanation?->dominantCauseCode,
            label: $reasonEnvelope?->operatorLabel ?? $operatorExplanation?->dominantCauseLabel ?? $artifactTruth?->primaryLabel,
            explanation: $reasonEnvelope?->shortExplanation ?? $operatorExplanation?->dominantCauseExplanation ?? $artifactTruth?->primaryExplanation,
            rank: $this->reasonRank($reasonEnvelope, $operatorExplanation),
        );

        match ($canonicalType) {
            'baseline.capture' => $this->baselineCaptureCandidates($candidates, $context),
            'baseline.compare' => $this->baselineCompareCandidates($candidates, $context),
            'tenant.evidence.snapshot.generate' => $this->evidenceSnapshotCandidates($candidates, $artifactTruth, $operatorExplanation),
            'environment.review.compose' => $this->reviewComposeCandidates($candidates, $artifactTruth),
            'environment.review_pack.generate' => $this->reviewPackCandidates($candidates, $artifactTruth),
            default => null,
        };

        usort($candidates, static function (array $left, array $right): int {
            $rank = ($right['rank'] <=> $left['rank']);

            if ($rank !== 0) {
                return $rank;
            }

            return strcmp($left['label'], $right['label']);
        });

        return array_values(array_map(
            static fn (array $candidate): array => [
                'code' => $candidate['code'],
                'label' => $candidate['label'],
                'explanation' => $candidate['explanation'],
            ],
            $candidates,
        ));
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string, rank: int}>  $candidates
     */
    private function pushCandidate(array &$candidates, ?string $code, ?string $label, ?string $explanation, int $rank): void
    {
        $label = is_string($label) ? trim($label) : '';
        $explanation = is_string($explanation) ? trim($explanation) : '';

        if ($label === '' || $explanation === '') {
            return;
        }

        foreach ($candidates as $candidate) {
            if (($candidate['label'] ?? null) === $label && ($candidate['explanation'] ?? null) === $explanation) {
                return;
            }
        }

        $candidates[] = [
            'code' => $code,
            'label' => $label,
            'explanation' => $explanation,
            'rank' => $rank,
        ];
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string, rank: int}>  $candidates
     * @param  array<string, mixed>  $context
     */
    private function baselineCaptureCandidates(array &$candidates, array $context): void
    {
        $reasonCode = (string) data_get($context, 'baseline_capture.reason_code', data_get($context, 'reason_code', ''));
        $subjectsTotal = $this->intValue(data_get($context, 'baseline_capture.subjects_total'));
        $gapCount = $this->intValue(data_get($context, 'baseline_capture.gaps.count'));
        $resumeToken = data_get($context, 'baseline_capture.resume_token');
        $changedAfterEnqueue = data_get($context, 'baseline_capture.eligibility.changed_after_enqueue') === true;

        if ($reasonCode === BaselineReasonCodes::CAPTURE_INVENTORY_MISSING) {
            $this->pushCandidate($candidates, $reasonCode, 'Run tenant sync first', 'No current inventory basis was available for this baseline capture.', 95);
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED) {
            $this->pushCandidate(
                $candidates,
                $reasonCode,
                'Latest inventory sync was blocked',
                $changedAfterEnqueue
                    ? 'The latest inventory sync changed after the run was queued and blocked the capture.'
                    : 'The latest inventory sync was blocked before this capture could produce a trustworthy baseline.',
                95,
            );
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_INVENTORY_FAILED) {
            $this->pushCandidate(
                $candidates,
                $reasonCode,
                'Latest inventory sync failed',
                $changedAfterEnqueue
                    ? 'The latest inventory sync failed after the run was queued, so the capture stopped without refreshing baseline truth.'
                    : 'The latest inventory sync failed before this capture could produce a trustworthy baseline.',
                95,
            );
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE) {
            $this->pushCandidate(
                $candidates,
                $reasonCode,
                'Latest inventory coverage unusable',
                $changedAfterEnqueue
                    ? 'The latest inventory coverage became unusable after the run was queued, so the capture stopped without refreshing baseline truth.'
                    : 'The latest inventory sync did not produce usable governed-subject coverage for this baseline capture.',
                95,
            );
        }

        if ($reasonCode === BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS) {
            $this->pushCandidate($candidates, $reasonCode, 'No subjects were in scope', 'No governed subjects were available for this baseline capture.', 95);
        }

        if ($subjectsTotal === 0) {
            $this->pushCandidate($candidates, 'no_subjects_in_scope', 'No governed subjects captured', 'No governed subjects were available for this baseline capture.', 95);
        }

        if ($gapCount > 0) {
            $this->pushCandidate($candidates, 'baseline_capture_gaps', 'Evidence gaps remain', "{$gapCount} governed subjects still need evidence capture before the snapshot is complete.", 82);
        }

        if (is_string($resumeToken) && trim($resumeToken) !== '') {
            $this->pushCandidate($candidates, 'baseline_capture_resume', 'Capture can resume', 'TenantPilot recorded a resume point because this capture could not finish in one pass.', 84);
        }
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string, rank: int}>  $candidates
     * @param  array<string, mixed>  $context
     */
    private function baselineCompareCandidates(array &$candidates, array $context): void
    {
        $reasonCode = (string) data_get($context, 'baseline_compare.reason_code', '');
        $gapCount = $this->intValue(data_get($context, 'baseline_compare.evidence_gaps.count'));
        $uncoveredTypes = array_values(array_filter((array) data_get($context, 'baseline_compare.coverage.uncovered_types', []), 'is_string'));
        $proof = data_get($context, 'baseline_compare.coverage.proof');
        $resumeToken = data_get($context, 'baseline_compare.resume_token');

        if ($reasonCode === BaselineCompareReasonCode::AmbiguousSubjects->value) {
            $this->pushCandidate($candidates, $reasonCode, 'Ambiguous matches', 'One or more governed subjects stayed ambiguous, so the compare result needs scope review.', 92);
        }

        if ($reasonCode === BaselineCompareReasonCode::StrategyFailed->value) {
            $this->pushCandidate($candidates, $reasonCode, 'Compare strategy failed', 'A compare strategy failed while processing in-scope governed subjects.', 94);
        }

        if ($gapCount > 0) {
            $this->pushCandidate($candidates, 'baseline_compare_gaps', 'Evidence gaps', "{$gapCount} governed subjects still have evidence gaps, so the compare output is incomplete.", 83);
        }

        if ($proof === false || $uncoveredTypes !== []) {
            $count = count($uncoveredTypes);
            $explanation = $count > 0
                ? "{$count} policy types were left without proven compare coverage."
                : 'Coverage proof was missing, so TenantPilot suppressed part of the normal compare output.';

            $this->pushCandidate($candidates, 'coverage_unproven', 'Coverage proof missing', $explanation, 81);
        }

        if (is_string($resumeToken) && trim($resumeToken) !== '') {
            $this->pushCandidate($candidates, 'baseline_compare_resume', 'Evidence capture needs to resume', 'The compare recorded a resume point because evidence capture did not finish in one pass.', 80);
        }
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string, rank: int}>  $candidates
     * @param  array<int, CountDescriptor>  $countDescriptors
     */
    private function evidenceSnapshotCandidates(array &$candidates, ?ArtifactTruthEnvelope $artifactTruth, ?OperatorExplanationPattern $operatorExplanation): void
    {
        $countDescriptors = $operatorExplanation?->countDescriptors ?? [];
        $missing = $this->findCountDescriptor($countDescriptors, 'Missing dimensions');
        $stale = $this->findCountDescriptor($countDescriptors, 'Stale dimensions');

        if ($missing instanceof CountDescriptor && $missing->value > 0) {
            $this->pushCandidate($candidates, 'missing_dimensions', 'Missing dimensions', "{$missing->value} evidence dimensions are still missing from this snapshot.", 88);
        }

        if ($artifactTruth?->freshnessState === 'stale' || ($stale instanceof CountDescriptor && $stale->value > 0)) {
            $value = $stale instanceof CountDescriptor && $stale->value > 0
                ? "{$stale->value} evidence dimensions are stale and should be refreshed."
                : 'Part of the evidence basis is stale and should be refreshed before use.';

            $this->pushCandidate($candidates, 'stale_evidence', 'Stale evidence basis', $value, 82);
        }
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string, rank: int}>  $candidates
     */
    private function reviewComposeCandidates(array &$candidates, ?ArtifactTruthEnvelope $artifactTruth): void
    {
        if ($artifactTruth?->contentState === 'partial') {
            $this->pushCandidate($candidates, 'review_missing_sections', 'Missing sections', 'Required review sections are still incomplete for this generated review.', 90);
        }

        if ($artifactTruth?->freshnessState === 'stale') {
            $this->pushCandidate($candidates, 'review_stale_evidence', 'Stale evidence basis', 'The review relies on stale evidence and needs a refreshed evidence basis.', 86);
        }

        if ($artifactTruth?->publicationReadiness === 'blocked') {
            $this->pushCandidate($candidates, 'review_blocked', 'Publication blocked', 'The review cannot move forward until its blocking prerequisites are cleared.', 95);
        }
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string, rank: int}>  $candidates
     */
    private function reviewPackCandidates(array &$candidates, ?ArtifactTruthEnvelope $artifactTruth): void
    {
        if ($artifactTruth?->publicationReadiness === 'blocked') {
            $this->pushCandidate($candidates, 'review_pack_blocked', 'Shareable pack not available', 'The pack did not produce a shareable artifact yet.', 94);
        }

        if ($artifactTruth?->publicationReadiness === 'internal_only') {
            $this->pushCandidate($candidates, 'review_pack_internal_only', 'Internal-only outcome', 'The pack can support internal follow-up, but it should not be shared externally yet.', 80);
        }

        if ($artifactTruth?->freshnessState === 'stale') {
            $this->pushCandidate($candidates, 'review_pack_stale_source', 'Source review is stale', 'The pack inherits stale review evidence and needs a refreshed source review.', 84);
        }

        if ($artifactTruth?->contentState === 'partial') {
            $this->pushCandidate($candidates, 'review_pack_partial_source', 'Source review is incomplete', 'The pack inherits incomplete source review content and needs follow-up before sharing.', 86);
        }
    }

    private function reasonRank(
        ?ReasonResolutionEnvelope $reasonEnvelope,
        ?OperatorExplanationPattern $operatorExplanation,
    ): int {
        if ($reasonEnvelope?->actionability === 'retryable_transient') {
            return 76;
        }

        return match ($operatorExplanation?->nextActionCategory) {
            'fix_prerequisite' => 92,
            'retry_later' => 76,
            'none' => 40,
            default => 85,
        };
    }

    /**
     * @param  list<array{code: ?string, label: string, explanation: string}>  $secondaryCauses
     * @return list<array{
     *     label: string,
     *     value: string,
     *     hint?: ?string,
     *     emphasis?: string
     * }>
     */
    private function secondaryFacts(
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        array $secondaryCauses,
        string $nextActionCategory,
        string $nextActionText,
    ): array {
        $facts = [];

        if ($operatorExplanation instanceof OperatorExplanationPattern) {
            $facts[] = [
                'label' => 'Result trust',
                'value' => $operatorExplanation->trustworthinessLabel(),
                'hint' => $this->deduplicateSecondaryFactHint(
                    $operatorExplanation->reliabilityStatement,
                    $operatorExplanation->dominantCauseExplanation,
                    $artifactTruth?->primaryExplanation,
                ),
                'emphasis' => $this->emphasisFromTrust($operatorExplanation->trustworthinessLevel->value),
            ];

            if ($operatorExplanation->evaluationResultLabel() !== '') {
                $facts[] = [
                    'label' => 'Result meaning',
                    'value' => $operatorExplanation->evaluationResultLabel(),
                    'hint' => $operatorExplanation->coverageStatement,
                    'emphasis' => 'neutral',
                ];
            }
        }

        if ($secondaryCauses !== []) {
            $facts[] = [
                'label' => 'Secondary causes',
                'value' => implode(' · ', array_map(static fn (array $cause): string => $cause['label'], $secondaryCauses)),
                'hint' => 'Additional contributing causes stay visible without replacing the dominant cause.',
                'emphasis' => 'caution',
            ];
        }

        if ($artifactTruth?->relatedArtifactUrl === null && $nextActionCategory !== 'no_further_action') {
            $facts[] = [
                'label' => 'Related artifact access',
                'value' => 'No related artifact link is available from this run.',
                'emphasis' => 'neutral',
            ];
        }

        return $facts;
    }

    private function emphasisFromTrust(string $trust): string
    {
        return match ($trust) {
            'unusable' => 'blocked',
            'diagnostic_only', 'limited_confidence' => 'caution',
            default => 'neutral',
        };
    }

    private function deduplicateSecondaryFactHint(?string $hint, ?string ...$duplicates): ?string
    {
        $normalizedHint = $this->normalizeFactText($hint);

        if ($normalizedHint === null) {
            return null;
        }

        foreach ($duplicates as $duplicate) {
            if ($normalizedHint === $this->normalizeFactText($duplicate)) {
                return null;
            }
        }

        return trim($hint ?? '');
    }

    private function fallbackCause(
        ?ArtifactTruthEnvelope $artifactTruth,
        ?OperatorExplanationPattern $operatorExplanation,
        ?ReasonResolutionEnvelope $reasonEnvelope,
    ): array {
        return [
            'code' => $reasonEnvelope?->internalCode ?? $operatorExplanation?->dominantCauseCode,
            'label' => $reasonEnvelope?->operatorLabel
                ?? $operatorExplanation?->dominantCauseLabel
                ?? $artifactTruth?->primaryLabel
                ?? 'Follow-up required',
            'explanation' => $reasonEnvelope?->shortExplanation
                ?? $operatorExplanation?->dominantCauseExplanation
                ?? $artifactTruth?->primaryExplanation
                ?? 'TenantPilot recorded enough detail to keep this run out of an all-clear state.',
        ];
    }

    private function findCountDescriptor(array $countDescriptors, string $label): ?CountDescriptor
    {
        foreach ($countDescriptors as $descriptor) {
            if ($descriptor instanceof CountDescriptor && $descriptor->label === $label) {
                return $descriptor;
            }
        }

        return null;
    }

    private function intValue(mixed $value): ?int
    {
        return is_numeric($value) ? (int) $value : null;
    }

    private function pluralizeDescriptor(CountDescriptor $descriptor): string
    {
        return match ($descriptor->label) {
            'Missing dimensions' => 'evidence dimensions are missing',
            'Stale dimensions' => 'evidence dimensions are stale',
            'Evidence dimensions' => 'evidence dimensions were recorded',
            'Sections' => 'sections were recorded',
            'Reports' => 'reports were recorded',
            'Findings' => 'findings were recorded',
            default => strtolower($descriptor->label).' were recorded',
        };
    }

    private function normalizeFactText(?string $value): ?string
    {
        if (! is_string($value)) {
            return null;
        }

        $normalized = trim((string) preg_replace('/\s+/', ' ', $value));

        if ($normalized === '') {
            return null;
        }

        return mb_strtolower($normalized);
    }
}
