<?php

declare(strict_types=1);

namespace App\Support\PortfolioCompare;

use App\Models\InventoryItem;
use App\Models\ManagedEnvironment;
use App\Services\Baselines\CurrentStateHashResolver;
use App\Services\Baselines\Evidence\EvidenceProvenance;
use App\Services\Baselines\Evidence\ResolvedEvidence;
use App\Support\Baselines\BaselineSubjectKey;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

final class CrossEnvironmentComparePreviewBuilder
{
    public function __construct(
        private readonly CurrentStateHashResolver $currentStateHashResolver,
    ) {}

    /**
     * @return array{
     *     selection: array{
     *         workspaceId: int,
     *         sourceEnvironmentId: int,
     *         sourceEnvironmentName: string,
     *         targetEnvironmentId: int,
     *         targetEnvironmentName: string,
     *         policyTypes: list<string>
     *     },
     *     summary: array{match: int, different: int, missing: int, ambiguous: int, blocked: int, total: int},
     *     subjects: list<array<string, mixed>>
     * }
     */
    public function build(CrossEnvironmentCompareSelection $selection): array
    {
        $sourceIndex = $this->indexEnvironmentSubjects($selection->sourceEnvironment, $selection->policyTypes);
        $targetIndex = $this->indexEnvironmentSubjects($selection->targetEnvironment, $selection->policyTypes);

        $sourceEvidence = $this->resolvedEvidenceMap($selection->sourceEnvironment, $sourceIndex['evidence_subjects']);
        $targetEvidence = $this->resolvedEvidenceMap($selection->targetEnvironment, $targetIndex['evidence_subjects']);

        $subjects = [];
        $summary = [
            'match' => 0,
            'different' => 0,
            'missing' => 0,
            'ambiguous' => 0,
            'blocked' => 0,
            'total' => 0,
        ];

        foreach ($sourceIndex['preview_subjects'] as $sourceSubject) {
            $previewSubject = $this->buildPreviewSubject(
                sourceSubject: $sourceSubject,
                sourceEnvironment: $selection->sourceEnvironment,
                targetEnvironment: $selection->targetEnvironment,
                targetIndex: $targetIndex['subjects'],
                sourceEvidence: $sourceEvidence,
                targetEvidence: $targetEvidence,
            );

            $subjects[] = $previewSubject;
            $summary[$previewSubject['state']]++;
            $summary['total']++;
        }

        usort($subjects, function (array $left, array $right): int {
            $policyTypeComparison = strcmp((string) ($left['policyType'] ?? ''), (string) ($right['policyType'] ?? ''));

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

            $displayNameComparison = strcmp(
                Str::lower((string) ($left['displayName'] ?? '')),
                Str::lower((string) ($right['displayName'] ?? '')),
            );

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

            return strcmp((string) ($left['subjectKey'] ?? ''), (string) ($right['subjectKey'] ?? ''));
        });

        return [
            'selection' => [
                'workspaceId' => $selection->workspaceId(),
                'sourceEnvironmentId' => $selection->sourceEnvironmentId(),
                'sourceEnvironmentName' => (string) $selection->sourceEnvironment->name,
                'targetEnvironmentId' => $selection->targetEnvironmentId(),
                'targetEnvironmentName' => (string) $selection->targetEnvironment->name,
                'policyTypes' => $selection->policyTypes,
            ],
            'summary' => $summary,
            'subjects' => $subjects,
        ];
    }

    /**
     * @param  ManagedEnvironment  $environment
     * @param  list<string>  $policyTypes
     * @return array{
     *     preview_subjects: list<array<string, mixed>>,
     *     evidence_subjects: list<array{policy_type: string, subject_external_id: string}>,
     *     subjects: array<string, array<string, mixed>>
     * }
     */
    private function indexEnvironmentSubjects(ManagedEnvironment $environment, array $policyTypes): array
    {
        $inventoryItems = InventoryItem::query()
            ->where('managed_environment_id', (int) $environment->getKey())
            ->when(
                $policyTypes !== [],
                fn ($query) => $query->whereIn('policy_type', $policyTypes),
            )
            ->orderBy('policy_type')
            ->orderBy('display_name')
            ->orderBy('id')
            ->get();

        $subjects = [];
        $previewSubjects = [];
        $evidenceSubjects = [];

        foreach ($inventoryItems as $inventoryItem) {
            if (! $inventoryItem instanceof InventoryItem) {
                continue;
            }

            $policyType = trim((string) $inventoryItem->policy_type);
            $subjectKey = BaselineSubjectKey::forPolicy(
                $policyType,
                is_string($inventoryItem->display_name ?? null) ? (string) $inventoryItem->display_name : null,
                is_string($inventoryItem->external_id ?? null) ? (string) $inventoryItem->external_id : null,
            );

            $subjectRecord = $this->inventorySubjectRecord($environment, $inventoryItem, $policyType, $subjectKey);

            if ($subjectKey === null) {
                $previewSubjects[] = [
                    ...$subjectRecord,
                    'resolution' => 'identifier_missing',
                    'duplicateCount' => 1,
                ];

                continue;
            }

            $indexKey = $this->subjectIndexKey($policyType, $subjectKey);

            if (! array_key_exists($indexKey, $subjects)) {
                $subjects[$indexKey] = [
                    'policyType' => $policyType,
                    'subjectKey' => $subjectKey,
                    'displayName' => $subjectRecord['displayName'],
                    'items' => [],
                ];
            }

            $subjects[$indexKey]['items'][] = $subjectRecord;
        }

        foreach ($subjects as $indexKey => $subjectGroup) {
            $items = is_array($subjectGroup['items'] ?? null) ? $subjectGroup['items'] : [];
            $firstItem = $items[0] ?? null;

            if (! is_array($firstItem)) {
                continue;
            }

            $previewSubjects[] = [
                ...$firstItem,
                'resolution' => count($items) > 1 ? 'ambiguous_match' : 'resolved',
                'duplicateCount' => count($items),
            ];

            if (count($items) === 1) {
                $evidenceSubjects[] = [
                    'policy_type' => (string) $firstItem['policyType'],
                    'subject_external_id' => (string) $firstItem['subjectExternalId'],
                ];
            }

            $subjects[$indexKey]['representative'] = $firstItem;
            $subjects[$indexKey]['duplicateCount'] = count($items);
        }

        return [
            'preview_subjects' => $previewSubjects,
            'evidence_subjects' => $evidenceSubjects,
            'subjects' => $subjects,
        ];
    }

    /**
     * @param  array<string, array<string, mixed>>  $targetIndex
     * @param  array<string, ResolvedEvidence|null>  $sourceEvidence
     * @param  array<string, ResolvedEvidence|null>  $targetEvidence
     * @return array<string, mixed>
     */
    private function buildPreviewSubject(
        array $sourceSubject,
        ManagedEnvironment $sourceEnvironment,
        ManagedEnvironment $targetEnvironment,
        array $targetIndex,
        array $sourceEvidence,
        array $targetEvidence,
    ): array {
        $policyType = (string) ($sourceSubject['policyType'] ?? '');
        $displayName = (string) ($sourceSubject['displayName'] ?? '');
        $subjectKey = is_string($sourceSubject['subjectKey'] ?? null) ? (string) $sourceSubject['subjectKey'] : null;
        $reasonCodes = [];
        $state = 'blocked';
        $trustLevel = 'unusable';

        $sourceEvidenceRecord = $this->resolvedEvidenceForSubject($sourceEvidence, $sourceSubject);
        $targetEvidenceRecord = null;
        $targetSubject = null;

        if (($sourceSubject['resolution'] ?? null) === 'identifier_missing') {
            $reasonCodes[] = 'source_identifier_missing';
        } elseif (($sourceSubject['resolution'] ?? null) === 'ambiguous_match') {
            $state = 'ambiguous';
            $trustLevel = 'diagnostic_only';
            $reasonCodes[] = 'source_subject_ambiguous';
        } elseif ($subjectKey !== null) {
            $targetSubject = $targetIndex[$this->subjectIndexKey($policyType, $subjectKey)] ?? null;

            if (! is_array($targetSubject)) {
                $state = 'missing';
                $trustLevel = $sourceEvidenceRecord instanceof ResolvedEvidence
                    && $sourceEvidenceRecord->fidelity === EvidenceProvenance::FidelityContent
                    ? 'trustworthy'
                    : 'limited_confidence';
                $reasonCodes[] = 'target_subject_missing';
            } elseif ((int) ($targetSubject['duplicateCount'] ?? 0) > 1) {
                $state = 'ambiguous';
                $trustLevel = 'diagnostic_only';
                $reasonCodes[] = 'target_subject_ambiguous';
            } else {
                $representative = $targetSubject['representative'] ?? null;

                if (is_array($representative)) {
                    $targetEvidenceRecord = $this->resolvedEvidenceForSubject($targetEvidence, $representative);
                }

                if (! $sourceEvidenceRecord instanceof ResolvedEvidence) {
                    $reasonCodes[] = 'source_evidence_refresh_required';
                }

                if (! $targetEvidenceRecord instanceof ResolvedEvidence) {
                    $reasonCodes[] = 'target_evidence_refresh_required';
                }

                if ($sourceEvidenceRecord instanceof ResolvedEvidence && $targetEvidenceRecord instanceof ResolvedEvidence) {
                    $state = $sourceEvidenceRecord->hash === $targetEvidenceRecord->hash ? 'match' : 'different';
                    $trustLevel = $sourceEvidenceRecord->fidelity === EvidenceProvenance::FidelityContent
                        && $targetEvidenceRecord->fidelity === EvidenceProvenance::FidelityContent
                        ? 'trustworthy'
                        : 'limited_confidence';

                    if ($sourceEvidenceRecord->fidelity !== EvidenceProvenance::FidelityContent) {
                        $reasonCodes[] = 'source_evidence_refresh_required';
                    }

                    if ($targetEvidenceRecord->fidelity !== EvidenceProvenance::FidelityContent) {
                        $reasonCodes[] = 'target_evidence_refresh_required';
                    }
                } else {
                    $state = 'blocked';
                    $trustLevel = 'unusable';
                }
            }
        }

        if ($state === 'missing' && ! $sourceEvidenceRecord instanceof ResolvedEvidence) {
            $reasonCodes[] = 'source_evidence_refresh_required';
        }

        if ($state === 'blocked' && $reasonCodes === []) {
            $reasonCodes[] = 'source_evidence_refresh_required';
        }

        $reasonCodes = array_values(array_unique($reasonCodes));

        return [
            'policyType' => $policyType,
            'displayName' => $displayName,
            'subjectKey' => $subjectKey,
            'state' => $state,
            'trustLevel' => $trustLevel,
            'reasonCodes' => $reasonCodes,
            'source' => $this->subjectSidePayload($sourceEnvironment, $sourceSubject, $sourceEvidenceRecord),
            'target' => $this->subjectSidePayload(
                $targetEnvironment,
                is_array($targetSubject['representative'] ?? null) ? $targetSubject['representative'] : null,
                $targetEvidenceRecord,
            ),
        ];
    }

    /**
     * @param  list<array{policy_type: string, subject_external_id: string}>  $subjects
     * @return array<string, ResolvedEvidence|null>
     */
    private function resolvedEvidenceMap(ManagedEnvironment $environment, array $subjects): array
    {
        return $this->currentStateHashResolver->resolveForSubjects($environment, $subjects);
    }

    /**
     * @param  array<string, mixed>|null  $subject
     * @return array<string, mixed>
     */
    private function subjectSidePayload(ManagedEnvironment $environment, ?array $subject, ?ResolvedEvidence $evidence): array
    {
        return [
            'environmentId' => (int) $environment->getKey(),
            'environmentName' => (string) $environment->name,
            'inventoryItemId' => is_numeric($subject['inventoryItemId'] ?? null) ? (int) $subject['inventoryItemId'] : null,
            'subjectExternalId' => is_string($subject['subjectExternalId'] ?? null) ? (string) $subject['subjectExternalId'] : null,
            'displayName' => is_string($subject['displayName'] ?? null) ? (string) $subject['displayName'] : null,
            'subjectKey' => is_string($subject['subjectKey'] ?? null) ? (string) $subject['subjectKey'] : null,
            'lastSeenAt' => is_string($subject['lastSeenAt'] ?? null) ? (string) $subject['lastSeenAt'] : null,
            'evidence' => $this->evidencePayload($evidence),
        ];
    }

    /**
     * @return array{
     *     policyType: string,
     *     displayName: string,
     *     subjectKey: ?string,
     *     inventoryItemId: int,
     *     subjectExternalId: string,
     *     lastSeenAt: ?string
     * }
     */
    private function inventorySubjectRecord(ManagedEnvironment $environment, InventoryItem $inventoryItem, string $policyType, ?string $subjectKey): array
    {
        $displayName = is_string($inventoryItem->display_name ?? null) ? trim((string) $inventoryItem->display_name) : '';
        $displayName = $displayName !== ''
            ? $displayName
            : ($subjectKey !== null ? Str::headline($subjectKey) : Str::headline($policyType));

        return [
            'environmentId' => (int) $environment->getKey(),
            'policyType' => $policyType,
            'displayName' => $displayName,
            'subjectKey' => $subjectKey,
            'inventoryItemId' => (int) $inventoryItem->getKey(),
            'subjectExternalId' => (string) $inventoryItem->external_id,
            'lastSeenAt' => $inventoryItem->last_seen_at?->toIso8601String(),
        ];
    }

    /**
     * @param  array<string, ResolvedEvidence|null>  $evidenceMap
     */
    private function resolvedEvidenceForSubject(array $evidenceMap, array $subject): ?ResolvedEvidence
    {
        $policyType = trim((string) ($subject['policyType'] ?? ''));
        $subjectExternalId = trim((string) ($subject['subjectExternalId'] ?? ''));

        if ($policyType === '' || $subjectExternalId === '') {
            return null;
        }

        $key = $policyType.'|'.$subjectExternalId;
        $evidence = $evidenceMap[$key] ?? null;

        return $evidence instanceof ResolvedEvidence ? $evidence : null;
    }

    /**
     * @return array{
     *     hash: string,
     *     fidelity: string,
     *     source: string,
     *     observedAt: ?string,
     *     policyVersionId: ?int,
     *     operationRunId: ?int,
     *     capturePurpose: ?string
     * }|null
     */
    private function evidencePayload(?ResolvedEvidence $evidence): ?array
    {
        if (! $evidence instanceof ResolvedEvidence) {
            return null;
        }

        return [
            'hash' => $evidence->hash,
            'fidelity' => $evidence->fidelity,
            'source' => $evidence->source,
            'observedAt' => $evidence->observedAt?->toIso8601String(),
            'policyVersionId' => is_numeric($evidence->meta['policy_version_id'] ?? null)
                ? (int) $evidence->meta['policy_version_id']
                : null,
            'operationRunId' => is_numeric($evidence->meta['operation_run_id'] ?? null)
                ? (int) $evidence->meta['operation_run_id']
                : null,
            'capturePurpose' => is_string($evidence->meta['capture_purpose'] ?? null)
                ? (string) $evidence->meta['capture_purpose']
                : null,
        ];
    }

    private function subjectIndexKey(string $policyType, string $subjectKey): string
    {
        return $policyType.'|'.$subjectKey;
    }
}
