<?php

declare(strict_types=1);

namespace Tests\Feature\Baselines\Support;

use App\Models\ManagedEnvironment;
use App\Services\Baselines\Evidence\EvidenceProvenance;
use App\Services\Baselines\Evidence\ResolvedEvidence;
use App\Support\Baselines\Compare\CompareFindingCandidate;
use App\Support\Baselines\Compare\CompareOrchestrationContext;
use App\Support\Baselines\Compare\CompareState;
use App\Support\Baselines\Compare\CompareStrategy;
use App\Support\Baselines\Compare\CompareStrategyCapability;
use App\Support\Baselines\Compare\CompareStrategyKey;
use App\Support\Baselines\Compare\CompareSubjectIdentity;
use App\Support\Baselines\Compare\CompareSubjectProjection;
use App\Support\Baselines\Compare\CompareSubjectResult;
use App\Support\Baselines\OperatorActionCategory;
use App\Support\Baselines\ResolutionOutcome;
use App\Support\Baselines\ResolutionPath;
use App\Support\Baselines\SubjectClass;
use App\Support\Governance\GovernanceDomainKey;
use App\Support\Governance\GovernanceSubjectClass;
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
use App\Support\Governance\GovernanceSubjectType;

final class FakeCompareStrategy implements CompareStrategy
{
    public function key(): CompareStrategyKey
    {
        return CompareStrategyKey::from('future_control');
    }

    public function capabilities(): array
    {
        return [
            new CompareStrategyCapability(
                strategyKey: $this->key(),
                domainKeys: [GovernanceDomainKey::Entra->value],
                subjectClasses: [GovernanceSubjectClass::Control->value],
                subjectTypeKeys: ['conditionalAccessPolicy'],
            ),
        ];
    }

    public function compare(
        CompareOrchestrationContext $context,
        ManagedEnvironment $tenant,
        array $baselineItems,
        array $currentItems,
        array $resolvedCurrentEvidence,
        array $severityMapping,
    ): array {
        $subjectResults = [];

        foreach ($baselineItems as $key => $baselineItem) {
            $currentItem = $currentItems[$key] ?? null;

            if (! is_array($currentItem)) {
                $subjectResults[] = $this->driftResult(
                    context: $context,
                    baselineItem: $baselineItem,
                    currentItem: null,
                    currentEvidence: null,
                    changeType: 'missing_policy',
                    severity: 'high',
                );

                continue;
            }

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

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

                continue;
            }

            $baselineMeta = is_array($baselineItem['meta_jsonb'] ?? null) ? $baselineItem['meta_jsonb'] : [];
            $currentMeta = is_array($currentItem['meta_jsonb'] ?? null) ? $currentItem['meta_jsonb'] : [];

            if ($this->metaFingerprint($baselineMeta) !== $this->metaFingerprint($currentMeta)) {
                $subjectResults[] = $this->driftResult(
                    context: $context,
                    baselineItem: $baselineItem,
                    currentItem: $currentItem,
                    currentEvidence: $currentEvidence,
                    changeType: 'different_version',
                    severity: 'medium',
                );

                continue;
            }

            $subjectResults[] = new CompareSubjectResult(
                subjectIdentity: $this->identity(
                    policyType: (string) $baselineItem['policy_type'],
                    externalSubjectId: (string) $baselineItem['subject_external_id'],
                    subjectKey: (string) $baselineItem['subject_key'],
                ),
                projection: $this->projection(
                    policyType: (string) $baselineItem['policy_type'],
                    operatorLabel: (string) (($currentItem['meta_jsonb']['display_name'] ?? $baselineItem['meta_jsonb']['display_name'] ?? $baselineItem['subject_key']) ?: $baselineItem['subject_key']),
                ),
                baselineAvailability: 'available',
                currentStateAvailability: 'available',
                compareState: CompareState::NoDrift,
                trustLevel: 'trustworthy',
                evidenceQuality: $currentEvidence->fidelity,
            );
        }

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

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

            if (! $currentEvidence instanceof ResolvedEvidence) {
                $subjectResults[] = $this->gapResult(
                    policyType: (string) $currentItem['policy_type'],
                    subjectKey: (string) $currentItem['subject_key'],
                    externalSubjectId: (string) $currentItem['subject_external_id'],
                    operatorLabel: (string) (($currentItem['meta_jsonb']['display_name'] ?? $currentItem['subject_key']) ?: $currentItem['subject_key']),
                    compareState: CompareState::Incomplete,
                    reasonCode: 'missing_current',
                    baselineAvailability: 'missing',
                    currentStateAvailability: 'unknown',
                    trustLevel: 'unusable',
                    evidenceQuality: 'missing',
                );

                continue;
            }

            $subjectResults[] = $this->driftResult(
                context: $context,
                baselineItem: null,
                currentItem: $currentItem,
                currentEvidence: $currentEvidence,
                changeType: 'unexpected_policy',
                severity: 'low',
            );
        }

        return [
            'subject_results' => $subjectResults,
            'diagnostics' => [
                'strategy_family' => 'future_control',
                'state_counts' => [
                    'drift' => count(array_filter($subjectResults, static fn (CompareSubjectResult $result): bool => $result->compareState === CompareState::Drift)),
                ],
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $meta
     */
    private function metaFingerprint(array $meta): string
    {
        unset($meta['display_name'], $meta['category'], $meta['platform']);

        return hash('sha256', json_encode($this->sortRecursive($meta), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    }

    /**
     * @param  array<string, mixed>  $value
     * @return array<string, mixed>
     */
    private function sortRecursive(array $value): array
    {
        foreach ($value as $key => $nestedValue) {
            if (! is_array($nestedValue)) {
                continue;
            }

            $value[$key] = $this->sortRecursive($nestedValue);
        }

        ksort($value, SORT_STRING);

        return $value;
    }

    private function driftResult(
        CompareOrchestrationContext $context,
        ?array $baselineItem,
        ?array $currentItem,
        ?ResolvedEvidence $currentEvidence,
        string $changeType,
        string $severity,
    ): CompareSubjectResult {
        $source = $baselineItem ?? $currentItem ?? [];
        $policyType = (string) ($source['policy_type'] ?? 'conditionalAccessPolicy');
        $subjectKey = (string) ($source['subject_key'] ?? 'unknown');
        $externalSubjectId = (string) ($source['subject_external_id'] ?? 'unknown');
        $operatorLabel = (string) ((($currentItem['meta_jsonb']['display_name'] ?? null) ?: ($baselineItem['meta_jsonb']['display_name'] ?? null) ?: $subjectKey) ?: $subjectKey);
        $fidelity = $currentEvidence?->fidelity ?? EvidenceProvenance::FidelityMeta;
        $baselineProvenance = EvidenceProvenance::build(
            fidelity: EvidenceProvenance::FidelityMeta,
            source: EvidenceProvenance::SourceInventory,
            observedAt: null,
            observedOperationRunId: null,
        );
        $currentProvenance = $currentEvidence?->tenantProvenance() ?? EvidenceProvenance::build(
            fidelity: $fidelity,
            source: EvidenceProvenance::SourceInventory,
            observedAt: null,
            observedOperationRunId: $context->inventorySyncRunId(),
        );

        return new CompareSubjectResult(
            subjectIdentity: $this->identity($policyType, $externalSubjectId, $subjectKey),
            projection: $this->projection($policyType, $operatorLabel),
            baselineAvailability: $baselineItem === null ? 'missing' : 'available',
            currentStateAvailability: $currentItem === null ? 'missing' : 'available',
            compareState: CompareState::Drift,
            trustLevel: $fidelity === EvidenceProvenance::FidelityContent ? 'trustworthy' : 'limited_confidence',
            evidenceQuality: $fidelity,
            severityRecommendation: $severity,
            findingCandidate: new CompareFindingCandidate(
                changeType: $changeType,
                severity: $severity,
                fingerprintBasis: [
                    'policy_type' => $policyType,
                    'subject_key' => $subjectKey,
                    'change_type' => $changeType,
                ],
                evidencePayload: [
                    'change_type' => $changeType,
                    'policy_type' => $policyType,
                    'subject_key' => $subjectKey,
                    'display_name' => $operatorLabel,
                    'summary' => ['kind' => 'control_snapshot'],
                    'baseline' => [
                        'hash' => $baselineItem['baseline_hash'] ?? null,
                        'provenance' => $baselineProvenance,
                    ],
                    'current' => [
                        'hash' => $currentEvidence?->hash,
                        'provenance' => $currentProvenance,
                    ],
                    'fidelity' => $fidelity,
                    'provenance' => [
                        'baseline_profile_id' => $context->baselineProfileId,
                        'baseline_snapshot_id' => $context->baselineSnapshotId,
                        'compare_operation_run_id' => $context->operationRunId,
                        'inventory_sync_run_id' => $context->inventorySyncRunId(),
                    ],
                ],
            ),
            diagnostics: [
                'strategy_key' => $this->key()->value,
            ],
        );
    }

    private function gapResult(
        string $policyType,
        string $subjectKey,
        string $externalSubjectId,
        string $operatorLabel,
        CompareState $compareState,
        string $reasonCode,
        string $baselineAvailability,
        string $currentStateAvailability,
        string $trustLevel,
        string $evidenceQuality,
    ): CompareSubjectResult {
        return new CompareSubjectResult(
            subjectIdentity: $this->identity($policyType, $externalSubjectId, $subjectKey),
            projection: $this->projection($policyType, $operatorLabel),
            baselineAvailability: $baselineAvailability,
            currentStateAvailability: $currentStateAvailability,
            compareState: $compareState,
            trustLevel: $trustLevel,
            evidenceQuality: $evidenceQuality,
            diagnostics: [
                'reason_code' => $reasonCode,
                'gap_record' => [
                    'policy_type' => $policyType,
                    'subject_key' => $subjectKey,
                    'subject_class' => SubjectClass::Derived->value,
                    'resolution_path' => ResolutionPath::Derived->value,
                    'resolution_outcome' => ResolutionOutcome::CaptureFailed->value,
                    'operator_action_category' => OperatorActionCategory::RunInventorySync->value,
                    'structural' => false,
                    'retryable' => $reasonCode === 'missing_current',
                    'reason_code' => $reasonCode,
                    'search_text' => strtolower(implode(' ', [$policyType, $subjectKey, $reasonCode])),
                ],
            ],
        );
    }

    private function identity(string $policyType, string $externalSubjectId, string $subjectKey): CompareSubjectIdentity
    {
        return new CompareSubjectIdentity(
            domainKey: GovernanceDomainKey::Entra->value,
            subjectClass: GovernanceSubjectClass::Control->value,
            subjectTypeKey: $policyType,
            externalSubjectId: $externalSubjectId,
            subjectKey: $subjectKey,
        );
    }

    private function projection(string $policyType, string $operatorLabel): CompareSubjectProjection
    {
        return new CompareSubjectProjection(
            platformSubjectClass: 'control',
            domainKey: GovernanceDomainKey::Entra->value,
            subjectTypeKey: $policyType,
            operatorLabel: $operatorLabel,
            summaryKind: 'control_snapshot',
        );
    }
}

final class FailingCompareStrategy implements CompareStrategy
{
    public function key(): CompareStrategyKey
    {
        return CompareStrategyKey::from('failing_control');
    }

    public function capabilities(): array
    {
        return [
            new CompareStrategyCapability(
                strategyKey: $this->key(),
                domainKeys: [GovernanceDomainKey::Entra->value],
                subjectClasses: [GovernanceSubjectClass::Control->value],
                subjectTypeKeys: ['conditionalAccessPolicy'],
            ),
        ];
    }

    public function compare(
        CompareOrchestrationContext $context,
        ManagedEnvironment $tenant,
        array $baselineItems,
        array $currentItems,
        array $resolvedCurrentEvidence,
        array $severityMapping,
    ): array {
        throw new \RuntimeException('Synthetic strategy failure for compare testing.');
    }
}

final class FakeGovernanceSubjectTaxonomyRegistry extends GovernanceSubjectTaxonomyRegistry
{
    public function all(): array
    {
        return array_values(array_merge(parent::all(), [
            new GovernanceSubjectType(
                domainKey: GovernanceDomainKey::Entra,
                subjectClass: GovernanceSubjectClass::Control,
                subjectTypeKey: 'conditionalAccessPolicy',
                label: 'Conditional Access Policy',
                description: 'Synthetic test-only future domain control',
                captureSupported: true,
                compareSupported: true,
                inventorySupported: true,
                active: true,
                supportMode: 'supported',
                legacyBucket: null,
            ),
        ]));
    }
}