<?php

declare(strict_types=1);

namespace App\Framework\Discovery\Resilience;

use App\Framework\Core\Events\EventDispatcher;
use App\Framework\DateTime\Clock;
use App\Framework\Discovery\Events\DiscoveryFailedEvent;
use App\Framework\Discovery\Exceptions\DiscoveryException;
use App\Framework\Discovery\Exceptions\RecoverableDiscoveryException;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger;

/**
 * Handles resilience patterns for Discovery operations
 *
 * Implements retry logic, circuit breaker patterns, and graceful
 * degradation to ensure Discovery system reliability under various
 * failure conditions.
 */
final class DiscoveryResilienceHandler
{
    private array $circuitBreakers = [];

    private array $retryAttempts = [];

    public function __construct(
        private readonly Clock $clock,
        private readonly ?Logger $logger = null,
        private readonly ?EventDispatcher $eventDispatcher = null,
        private readonly int $maxRetries = 3,
        private readonly int $baseBackoffMs = 1000,
        private readonly int $maxBackoffMs = 30000,
        private readonly int $circuitBreakerThreshold = 5,
        private readonly int $circuitBreakerTimeoutSeconds = 60
    ) {
    }

    /**
     * Execute discovery operation with resilience patterns
     */
    public function executeWithResilience(
        callable $operation,
        DiscoveryContext $context,
        array $options = []
    ): DiscoveryRegistry {
        $operationId = $this->generateOperationId($context);
        $circuitBreakerKey = $this->getCircuitBreakerKey($context);

        // Check circuit breaker
        if ($this->isCircuitBreakerOpen($circuitBreakerKey)) {
            $this->logger?->warning('Discovery operation blocked by circuit breaker', [
                'operation_id' => $operationId,
                'circuit_breaker' => $circuitBreakerKey,
                'context' => $context->toArray(),
            ]);

            return $this->handleCircuitBreakerOpen($context, $options);
        }

        $maxRetries = $options['max_retries'] ?? $this->maxRetries;
        $attempt = 0;
        $lastException = null;

        while ($attempt <= $maxRetries) {
            try {
                $this->logger?->debug('Executing discovery operation', [
                    'operation_id' => $operationId,
                    'attempt' => $attempt + 1,
                    'max_attempts' => $maxRetries + 1,
                ]);

                $result = $operation();

                // Success - reset circuit breaker
                $this->recordSuccess($circuitBreakerKey);

                if ($attempt > 0) {
                    $this->logger?->info('Discovery operation succeeded after retry', [
                        'operation_id' => $operationId,
                        'successful_attempt' => $attempt + 1,
                        'total_attempts' => $attempt + 1,
                    ]);
                }

                return $result;

            } catch (FrameworkException $e) {
                $lastException = $e;
                $this->recordFailure($circuitBreakerKey);

                // Check if this is a recoverable error
                if (! $this->isRecoverableError($e) || $attempt >= $maxRetries) {
                    $this->logger?->error('Discovery operation failed - not recoverable or max retries reached', [
                        'operation_id' => $operationId,
                        'attempt' => $attempt + 1,
                        'error' => $e->getMessage(),
                        'error_code' => $e->getErrorCode()->value ?? 'unknown',
                        'is_recoverable' => $this->isRecoverableError($e),
                        'max_retries_reached' => $attempt >= $maxRetries,
                    ]);

                    break;
                }

                // Calculate backoff delay
                $backoffMs = $this->calculateBackoff($attempt, $e);

                $this->logger?->warning('Discovery operation failed - will retry', [
                    'operation_id' => $operationId,
                    'attempt' => $attempt + 1,
                    'error' => $e->getMessage(),
                    'retry_after_ms' => $backoffMs,
                    'next_attempt' => $attempt + 2,
                ]);

                // Wait before retry
                if ($backoffMs > 0) {
                    usleep($backoffMs * 1000);
                }

                $attempt++;

            } catch (\Throwable $e) {
                $lastException = $e;
                $this->recordFailure($circuitBreakerKey);

                $this->logger?->error('Discovery operation failed with unexpected error', [
                    'operation_id' => $operationId,
                    'attempt' => $attempt + 1,
                    'error' => $e->getMessage(),
                    'error_type' => get_class($e),
                ]);

                // Unexpected errors are generally not recoverable
                break;
            }
        }

        // All retries exhausted - emit failure event and handle gracefully
        $this->emitFailureEvent($context, $lastException);

        return $this->handleGracefulDegradation($context, $lastException, $options);
    }

    /**
     * Check if an error is recoverable
     */
    private function isRecoverableError(\Throwable $error): bool
    {
        // RecoverableDiscoveryException is always recoverable
        if ($error instanceof RecoverableDiscoveryException) {
            return true;
        }

        // Check FrameworkException error codes
        if ($error instanceof FrameworkException) {
            $errorCode = $error->getErrorCode();

            return match ($errorCode?->value ?? '') {
                'MEMORY_LIMIT_EXCEEDED',
                'OPERATION_TIMEOUT',
                'RESOURCE_CONFLICT',
                'FILESYSTEM_READ_ERROR',
                'CACHE_OPERATION_FAILED',
                'RESOURCE_LIMIT_EXCEEDED' => true,

                'PERMISSION_DENIED',
                'DATA_CORRUPTION_DETECTED',
                'DEPENDENCY_MISSING',
                'VAL_CONFIGURATION_INVALID' => false,

                default => $error->isRecoverable()
            };
        }

        // Check common PHP errors
        if ($error instanceof \ErrorException) {
            return match ($error->getSeverity()) {
                E_WARNING, E_NOTICE, E_USER_WARNING, E_USER_NOTICE => true,
                E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR => false,
                default => false
            };
        }

        // Specific exception types
        return match (get_class($error)) {
            \RuntimeException::class => true,
            \LogicException::class,
            \InvalidArgumentException::class,
            \BadMethodCallException::class => false,
            default => false
        };
    }

    /**
     * Calculate backoff delay with exponential backoff and jitter
     */
    private function calculateBackoff(int $attempt, \Throwable $error): int
    {
        // Use specific retry delay from RecoverableDiscoveryException
        if ($error instanceof RecoverableDiscoveryException) {
            return $error->retryAfterSeconds * 1000;
        }

        // Use retry hint from FrameworkException
        if ($error instanceof FrameworkException && $error->getRetryAfter() !== null) {
            return $error->getRetryAfter() * 1000;
        }

        // Exponential backoff with jitter
        $exponentialBackoff = $this->baseBackoffMs * (2 ** $attempt);
        $jitter = mt_rand(0, (int) ($exponentialBackoff * 0.1)); // 10% jitter
        $backoff = $exponentialBackoff + $jitter;

        return min($backoff, $this->maxBackoffMs);
    }

    /**
     * Check if circuit breaker is open
     */
    private function isCircuitBreakerOpen(string $key): bool
    {
        if (! isset($this->circuitBreakers[$key])) {
            return false;
        }

        $breaker = $this->circuitBreakers[$key];

        // If circuit breaker is in timeout, check if we should close it
        if ($breaker['state'] === 'open') {
            $timeoutExpired = ($this->clock->time() - $breaker['opened_at']) >= $this->circuitBreakerTimeoutSeconds;

            if ($timeoutExpired) {
                $this->circuitBreakers[$key]['state'] = 'half_open';
                $this->logger?->info('Circuit breaker moved to half-open state', [
                    'circuit_breaker' => $key,
                    'timeout_seconds' => $this->circuitBreakerTimeoutSeconds,
                ]);

                return false;
            }

            return true;
        }

        return false;
    }

    /**
     * Record successful operation
     */
    private function recordSuccess(string $circuitBreakerKey): void
    {
        if (isset($this->circuitBreakers[$circuitBreakerKey])) {
            $this->circuitBreakers[$circuitBreakerKey] = [
                'state' => 'closed',
                'failure_count' => 0,
                'last_success' => $this->clock->time(),
            ];

            $this->logger?->debug('Circuit breaker reset to closed state', [
                'circuit_breaker' => $circuitBreakerKey,
            ]);
        }
    }

    /**
     * Record failed operation
     */
    private function recordFailure(string $circuitBreakerKey): void
    {
        if (! isset($this->circuitBreakers[$circuitBreakerKey])) {
            $this->circuitBreakers[$circuitBreakerKey] = [
                'state' => 'closed',
                'failure_count' => 0,
                'last_failure' => null,
                'opened_at' => null,
            ];
        }

        $this->circuitBreakers[$circuitBreakerKey]['failure_count']++;
        $this->circuitBreakers[$circuitBreakerKey]['last_failure'] = $this->clock->time();

        // Open circuit breaker if threshold exceeded
        if ($this->circuitBreakers[$circuitBreakerKey]['failure_count'] >= $this->circuitBreakerThreshold) {
            $this->circuitBreakers[$circuitBreakerKey]['state'] = 'open';
            $this->circuitBreakers[$circuitBreakerKey]['opened_at'] = $this->clock->time();

            $this->logger?->warning('Circuit breaker opened due to excessive failures', [
                'circuit_breaker' => $circuitBreakerKey,
                'failure_count' => $this->circuitBreakers[$circuitBreakerKey]['failure_count'],
                'threshold' => $this->circuitBreakerThreshold,
                'timeout_seconds' => $this->circuitBreakerTimeoutSeconds,
            ]);
        }
    }

    /**
     * Handle circuit breaker open state
     */
    private function handleCircuitBreakerOpen(DiscoveryContext $context, array $options): DiscoveryRegistry
    {
        // Try to return cached results if available
        if ($options['fallback_to_cache'] ?? true) {
            $cachedResult = $this->tryGetCachedResult($context);
            if ($cachedResult !== null) {
                $this->logger?->info('Using cached results due to circuit breaker', [
                    'context' => $context->getCacheKey(),
                ]);

                return $cachedResult;
            }
        }

        // Return minimal/empty registry as last resort
        return $this->createMinimalRegistry($context);
    }

    /**
     * Handle graceful degradation
     */
    private function handleGracefulDegradation(
        DiscoveryContext $context,
        ?\Throwable $lastException,
        array $options
    ): DiscoveryRegistry {
        $this->logger?->warning('Discovery operation failed - attempting graceful degradation', [
            'context' => $context->getCacheKey(),
            'last_error' => $lastException?->getMessage(),
        ]);

        // Try cached results first
        if ($options['fallback_to_cache'] ?? true) {
            $cachedResult = $this->tryGetCachedResult($context);
            if ($cachedResult !== null) {
                $this->logger?->info('Using stale cached results for graceful degradation');

                return $cachedResult;
            }
        }

        // Try partial discovery with reduced scope
        if ($options['allow_partial_results'] ?? false) {
            try {
                return $this->attemptPartialDiscovery($context);
            } catch (\Throwable $e) {
                $this->logger?->warning('Partial discovery also failed', [
                    'error' => $e->getMessage(),
                ]);
            }
        }

        // Final fallback - throw the original exception or create minimal registry
        if ($options['throw_on_failure'] ?? false) {
            throw $lastException ?? new DiscoveryException('Discovery operation failed with unknown error');
        }

        return $this->createMinimalRegistry($context);
    }

    /**
     * Try to get cached results (even stale ones)
     */
    private function tryGetCachedResult(DiscoveryContext $context): ?DiscoveryRegistry
    {
        // This would integrate with the cache manager
        // For now, return null (no cached result available)
        return null;
    }

    /**
     * Attempt partial discovery with reduced scope
     */
    private function attemptPartialDiscovery(DiscoveryContext $context): DiscoveryRegistry
    {
        // This would implement a simplified discovery process
        // For now, return minimal registry
        return $this->createMinimalRegistry($context);
    }

    /**
     * Create minimal registry for fallback scenarios
     */
    private function createMinimalRegistry(DiscoveryContext $context): DiscoveryRegistry
    {
        $registry = new DiscoveryRegistry();

        $this->logger?->info('Created minimal discovery registry as fallback', [
            'context' => $context->getCacheKey(),
            'registry_empty' => count($registry) === 0,
        ]);

        return $registry;
    }

    /**
     * Generate unique operation ID
     */
    private function generateOperationId(DiscoveryContext $context): string
    {
        return 'discovery_' . md5($context->getCacheKey() . '_' . $this->clock->time());
    }

    /**
     * Get circuit breaker key for context
     */
    private function getCircuitBreakerKey(DiscoveryContext $context): string
    {
        // Create circuit breaker key based on operation type and scope
        return 'discovery_' . $context->scanType->value . '_' . md5(implode(':', $context->paths));
    }

    /**
     * Emit failure event
     */
    private function emitFailureEvent(DiscoveryContext $context, ?\Throwable $exception): void
    {
        if ($this->eventDispatcher === null || $exception === null) {
            return;
        }

        $this->eventDispatcher->dispatch(new DiscoveryFailedEvent(
            exception: $exception,
            partialResults: null,
            scanType: $context->scanType,
            timestamp: $this->clock->time()
        ));
    }

    /**
     * Get resilience statistics
     */
    public function getStatistics(): array
    {
        $stats = [
            'circuit_breakers' => [],
            'total_circuit_breakers' => count($this->circuitBreakers),
            'open_circuit_breakers' => 0,
            'half_open_circuit_breakers' => 0,
            'configuration' => [
                'max_retries' => $this->maxRetries,
                'base_backoff_ms' => $this->baseBackoffMs,
                'max_backoff_ms' => $this->maxBackoffMs,
                'circuit_breaker_threshold' => $this->circuitBreakerThreshold,
                'circuit_breaker_timeout_seconds' => $this->circuitBreakerTimeoutSeconds,
            ],
        ];

        foreach ($this->circuitBreakers as $key => $breaker) {
            $stats['circuit_breakers'][$key] = [
                'state' => $breaker['state'],
                'failure_count' => $breaker['failure_count'],
                'last_failure' => $breaker['last_failure'],
                'opened_at' => $breaker['opened_at'] ?? null,
            ];

            if ($breaker['state'] === 'open') {
                $stats['open_circuit_breakers']++;
            } elseif ($breaker['state'] === 'half_open') {
                $stats['half_open_circuit_breakers']++;
            }
        }

        return $stats;
    }

    /**
     * Reset circuit breaker
     */
    public function resetCircuitBreaker(string $key): bool
    {
        if (isset($this->circuitBreakers[$key])) {
            unset($this->circuitBreakers[$key]);
            $this->logger?->info('Circuit breaker manually reset', ['circuit_breaker' => $key]);

            return true;
        }

        return false;
    }

    /**
     * Reset all circuit breakers
     */
    public function resetAllCircuitBreakers(): int
    {
        $count = count($this->circuitBreakers);
        $this->circuitBreakers = [];
        $this->logger?->info('All circuit breakers manually reset', ['count' => $count]);

        return $count;
    }
}
