<?php

declare(strict_types=1);

namespace App\Support\Baselines;

use App\Support\Inventory\InventoryPolicyTypeMeta;

final class SubjectResolver
{
    public function capability(string $policyType): SupportCapabilityRecord
    {
        $contract = InventoryPolicyTypeMeta::baselineSupportContract($policyType);

        return new SupportCapabilityRecord(
            policyType: $policyType,
            subjectClass: SubjectClass::from($contract['subject_class']),
            compareCapability: $contract['compare_capability'],
            captureCapability: $contract['capture_capability'],
            resolutionPath: ResolutionPath::from($contract['resolution_path']),
            configSupported: (bool) $contract['config_supported'],
            runtimeValid: (bool) $contract['runtime_valid'],
            sourceModelExpected: $contract['source_model_expected'],
        );
    }

    public function describeForCompare(string $policyType, ?string $subjectExternalId = null, ?string $subjectKey = null): SubjectDescriptor
    {
        return $this->describe(operation: 'compare', policyType: $policyType, subjectExternalId: $subjectExternalId, subjectKey: $subjectKey);
    }

    public function describeForCapture(string $policyType, ?string $subjectExternalId = null, ?string $subjectKey = null): SubjectDescriptor
    {
        return $this->describe(operation: 'capture', policyType: $policyType, subjectExternalId: $subjectExternalId, subjectKey: $subjectKey);
    }

    public function resolved(SubjectDescriptor $descriptor, ?string $sourceModelFound = null): ResolutionOutcomeRecord
    {
        $outcome = $descriptor->expectsPolicy()
            ? ResolutionOutcome::ResolvedPolicy
            : ResolutionOutcome::ResolvedInventory;

        return new ResolutionOutcomeRecord(
            resolutionOutcome: $outcome,
            reasonCode: $outcome->value,
            operatorActionCategory: OperatorActionCategory::None,
            structural: false,
            retryable: false,
            sourceModelExpected: $descriptor->sourceModelExpected,
            sourceModelFound: $sourceModelFound ?? $descriptor->sourceModelExpected,
        );
    }

    public function missingExpectedRecord(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        $expectsPolicy = $descriptor->expectsPolicy();

        return new ResolutionOutcomeRecord(
            resolutionOutcome: $expectsPolicy ? ResolutionOutcome::PolicyRecordMissing : ResolutionOutcome::InventoryRecordMissing,
            reasonCode: $expectsPolicy ? 'policy_record_missing' : 'inventory_record_missing',
            operatorActionCategory: $expectsPolicy ? OperatorActionCategory::RunPolicySyncOrBackup : OperatorActionCategory::RunInventorySync,
            structural: false,
            retryable: false,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    public function structuralInventoryOnly(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::FoundationInventoryOnly,
            reasonCode: 'foundation_not_policy_backed',
            operatorActionCategory: OperatorActionCategory::ProductFollowUp,
            structural: true,
            retryable: false,
            sourceModelExpected: $descriptor->sourceModelExpected,
            sourceModelFound: 'inventory',
        );
    }

    public function invalidSubject(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::InvalidSubject,
            reasonCode: 'invalid_subject',
            operatorActionCategory: OperatorActionCategory::InspectSubjectMapping,
            structural: false,
            retryable: false,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    public function duplicateSubject(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::DuplicateSubject,
            reasonCode: 'duplicate_subject',
            operatorActionCategory: OperatorActionCategory::InspectSubjectMapping,
            structural: false,
            retryable: false,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    public function ambiguousMatch(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::AmbiguousMatch,
            reasonCode: 'ambiguous_match',
            operatorActionCategory: OperatorActionCategory::InspectSubjectMapping,
            structural: false,
            retryable: false,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    public function invalidSupportConfiguration(SupportCapabilityRecord $capability): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::InvalidSupportConfig,
            reasonCode: 'invalid_support_config',
            operatorActionCategory: OperatorActionCategory::ProductFollowUp,
            structural: true,
            retryable: false,
            sourceModelExpected: $capability->sourceModelExpected,
        );
    }

    public function throttled(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::Throttled,
            reasonCode: 'throttled',
            operatorActionCategory: OperatorActionCategory::Retry,
            structural: false,
            retryable: true,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    public function captureFailed(SubjectDescriptor $descriptor, bool $retryable = false): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: $retryable ? ResolutionOutcome::RetryableCaptureFailure : ResolutionOutcome::CaptureFailed,
            reasonCode: $retryable ? 'retryable_capture_failure' : 'capture_failed',
            operatorActionCategory: OperatorActionCategory::Retry,
            structural: false,
            retryable: $retryable,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    public function budgetExhausted(SubjectDescriptor $descriptor): ResolutionOutcomeRecord
    {
        return new ResolutionOutcomeRecord(
            resolutionOutcome: ResolutionOutcome::BudgetExhausted,
            reasonCode: 'budget_exhausted',
            operatorActionCategory: OperatorActionCategory::Retry,
            structural: false,
            retryable: true,
            sourceModelExpected: $descriptor->sourceModelExpected,
        );
    }

    private function describe(string $operation, string $policyType, ?string $subjectExternalId = null, ?string $subjectKey = null): SubjectDescriptor
    {
        $capability = $this->capability($policyType);
        $resolvedSubjectKey = $this->normalizeSubjectKey($policyType, $subjectExternalId, $subjectKey);

        return new SubjectDescriptor(
            policyType: $policyType,
            subjectExternalId: $subjectExternalId !== null && trim($subjectExternalId) !== '' ? trim($subjectExternalId) : null,
            subjectKey: $resolvedSubjectKey,
            subjectClass: $capability->subjectClass,
            resolutionPath: $capability->resolutionPath,
            supportMode: $capability->supportModeFor($operation),
            sourceModelExpected: $capability->sourceModelExpected,
        );
    }

    private function normalizeSubjectKey(string $policyType, ?string $subjectExternalId, ?string $subjectKey): string
    {
        $trimmedSubjectKey = is_string($subjectKey) ? trim($subjectKey) : '';

        if ($trimmedSubjectKey !== '') {
            return $trimmedSubjectKey;
        }

        $generated = BaselineSubjectKey::forPolicy($policyType, subjectExternalId: $subjectExternalId);

        if (is_string($generated) && $generated !== '') {
            return $generated;
        }

        $fallbackExternalId = is_string($subjectExternalId) && trim($subjectExternalId) !== ''
            ? trim($subjectExternalId)
            : 'unknown';

        return trim($policyType).'|'.$fallbackExternalId;
    }
}
