<?php

declare(strict_types=1);

namespace App\Support\PortfolioCompare;

final class CrossEnvironmentPromotionPreflight
{
    /**
     * @param  array{
     *     selection?: array<string, mixed>,
     *     subjects?: list<array<string, mixed>>
     * }  $preview
     * @return array{
     *     selection: array<string, mixed>,
     *     summary: array{ready: int, blocked: int, manual_mapping_required: int, total: int},
     *     blockedReasonCounts: array<string, int>,
     *     buckets: array{
     *         ready: list<array<string, mixed>>,
     *         blocked: list<array<string, mixed>>,
     *         manual_mapping_required: list<array<string, mixed>>
     *     }
     * }
     */
    public function build(array $preview): array
    {
        $subjects = is_array($preview['subjects'] ?? null) ? $preview['subjects'] : [];
        $buckets = [
            'ready' => [],
            'blocked' => [],
            'manual_mapping_required' => [],
        ];
        $blockedReasonCounts = [];

        foreach ($subjects as $subject) {
            if (! is_array($subject)) {
                continue;
            }

            $decision = $this->classifySubject($subject);
            $subject['preflight'] = $decision;
            $buckets[$decision['bucket']][] = $subject;

            if ($decision['bucket'] !== 'ready') {
                foreach ($decision['reasonCodes'] as $reasonCode) {
                    $blockedReasonCounts[$reasonCode] = ($blockedReasonCounts[$reasonCode] ?? 0) + 1;
                }
            }
        }

        return [
            'selection' => is_array($preview['selection'] ?? null) ? $preview['selection'] : [],
            'summary' => [
                'ready' => count($buckets['ready']),
                'blocked' => count($buckets['blocked']),
                'manual_mapping_required' => count($buckets['manual_mapping_required']),
                'total' => count($buckets['ready']) + count($buckets['blocked']) + count($buckets['manual_mapping_required']),
            ],
            'blockedReasonCounts' => $blockedReasonCounts,
            'buckets' => $buckets,
        ];
    }

    /**
     * @param  array<string, mixed>  $subject
     * @return array{bucket: 'ready'|'blocked'|'manual_mapping_required', reasonCodes: list<string>, reasonLabels: list<string>}
     */
    private function classifySubject(array $subject): array
    {
        $state = is_string($subject['state'] ?? null) ? (string) $subject['state'] : 'blocked';
        $reasonCodes = is_array($subject['reasonCodes'] ?? null)
            ? array_values(array_filter($subject['reasonCodes'], 'is_string'))
            : [];

        if (in_array('source_identifier_missing', $reasonCodes, true)) {
            return $this->decision('blocked', ['source_identifier_missing']);
        }

        if (in_array('source_subject_ambiguous', $reasonCodes, true)) {
            return $this->decision('blocked', ['source_subject_ambiguous']);
        }

        if (in_array('target_subject_ambiguous', $reasonCodes, true) || $state === 'ambiguous') {
            return $this->decision('manual_mapping_required', ['target_subject_ambiguous']);
        }

        $sourceEvidence = is_array(data_get($subject, 'source.evidence')) ? data_get($subject, 'source.evidence') : null;
        $targetEvidence = is_array(data_get($subject, 'target.evidence')) ? data_get($subject, 'target.evidence') : null;

        if (! $this->evidenceSupportsPromotion($sourceEvidence)) {
            return $this->decision('blocked', ['source_evidence_refresh_required']);
        }

        if ($state !== 'missing' && ! $this->evidenceSupportsPromotion($targetEvidence)) {
            return $this->decision('blocked', ['target_evidence_refresh_required']);
        }

        return match ($state) {
            'match' => $this->decision('ready', ['target_already_aligned']),
            'different' => $this->decision('ready', ['target_subject_requires_update']),
            'missing' => $this->decision('ready', ['target_subject_missing']),
            default => $this->decision('blocked', ['source_evidence_refresh_required']),
        };
    }

    /**
     * @param  array<string, mixed>|null  $evidence
     */
    private function evidenceSupportsPromotion(?array $evidence): bool
    {
        return is_array($evidence)
            && is_string($evidence['fidelity'] ?? null)
            && (string) $evidence['fidelity'] === 'content';
    }

    /**
     * @param  list<string>  $reasonCodes
     * @return array{bucket: 'ready'|'blocked'|'manual_mapping_required', reasonCodes: list<string>, reasonLabels: list<string>}
     */
    private function decision(string $bucket, array $reasonCodes): array
    {
        return [
            'bucket' => $bucket,
            'reasonCodes' => $reasonCodes,
            'reasonLabels' => array_map(fn (string $reasonCode): string => $this->reasonLabel($reasonCode), $reasonCodes),
        ];
    }

    private function reasonLabel(string $reasonCode): string
    {
        return match ($reasonCode) {
            'source_identifier_missing' => 'Source environment subject is missing a stable compare identifier.',
            'source_subject_ambiguous' => 'Source environment subject resolves to multiple candidates and cannot drive promotion safely.',
            'target_subject_ambiguous' => 'Target environment has multiple matching subjects and needs manual mapping.',
            'source_evidence_refresh_required' => 'Refresh source evidence before relying on this subject.',
            'target_evidence_refresh_required' => 'Refresh target evidence before relying on this subject.',
            'target_already_aligned' => 'Target environment already matches the source for this subject.',
            'target_subject_requires_update' => 'Target environment differs from the source but has enough evidence for later promotion planning.',
            'target_subject_missing' => 'Target environment does not currently contain this subject and can be planned as a missing target item.',
            default => 'This subject needs additional review before promotion planning can continue.',
        };
    }
}
