<?php

declare(strict_types=1);

namespace App\Framework\ErrorBoundaries\Async;

use App\Framework\Async\AsyncPromise;
use App\Framework\Async\FiberManager;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionFailed;
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionSucceeded;
use App\Framework\ErrorBoundaries\Events\BoundaryFallbackExecuted;
use App\Framework\ErrorBoundaries\Events\BoundaryTimeoutOccurred;
use App\Framework\Logging\Logger;
use Throwable;

/**
 * Async Error Boundary using framework's async components
 */
final readonly class AsyncErrorBoundary
{
    public function __construct(
        private string $boundaryName,
        private BoundaryConfig $config,
        private FiberManager $fiberManager,
        private Clock $clock,
        private Timer $timer,
        private ?Logger $logger = null,
        private ?BoundaryCircuitBreakerManager $circuitBreakerManager = null,
        private ?BoundaryEventPublisher $eventPublisher = null,
    ) {
    }

    /**
     * Execute async operation with error boundary
     *
     * @template T
     * @param callable(): T $operation
     * @param callable(): T|null $fallback
     * @return AsyncPromise<T>
     */
    public function executeAsync(callable $operation, ?callable $fallback = null): AsyncPromise
    {
        return AsyncPromise::create(function () use ($operation, $fallback) {
            $startTime = $this->clock->time();

            try {
                $result = $this->executeWithRetryAsync($operation);
                $executionTime = $startTime->age($this->clock);

                // Publish success event
                $this->publishEvent(new BoundaryExecutionSucceeded(
                    boundaryName: $this->boundaryName,
                    executionTime: $executionTime,
                    message: 'Async operation completed successfully',
                ));

                return $result;

            } catch (Throwable $e) {
                $executionTime = $startTime->age($this->clock);

                // Publish failure event
                $this->publishEvent(new BoundaryExecutionFailed(
                    boundaryName: $this->boundaryName,
                    exception: $e,
                    executionTime: $executionTime,
                    willRetry: false,
                    message: 'Async operation failed',
                ));

                return $this->handleFailureAsync($e, $fallback);
            }
        });
    }

    /**
     * Execute multiple async operations concurrently with individual boundaries
     *
     * @param array<string, callable> $operations
     * @param array<string, callable>|null $fallbacks
     * @return AsyncPromise<array>
     */
    public function executeConcurrent(array $operations, ?array $fallbacks = null): AsyncPromise
    {
        $promises = [];

        foreach ($operations as $name => $operation) {
            $fallback = $fallbacks[$name] ?? null;
            $promises[$name] = $this->executeAsync($operation, $fallback);
        }

        return AsyncPromise::all($promises);
    }

    /**
     * Execute with timeout using framework components
     *
     * @template T
     * @param callable(): T $operation
     * @param Duration $timeout
     * @param callable(): T|null $fallback
     * @return AsyncPromise<T>
     */
    public function executeWithTimeout(
        callable $operation,
        Duration $timeout,
        ?callable $fallback = null
    ): AsyncPromise {
        return AsyncPromise::create(function () use ($operation, $timeout, $fallback) {
            try {
                return $this->fiberManager->withTimeoutDuration($operation, $timeout);
            } catch (Throwable $e) {
                $this->publishEvent(new BoundaryTimeoutOccurred(
                    boundaryName: $this->boundaryName,
                    timeoutThreshold: $timeout,
                    actualExecutionTime: $timeout, // We don't know the exact time here
                    fallbackExecuted: $fallback !== null,
                    message: "Async operation timed out after {$timeout->toHumanReadable()}",
                ));

                if ($fallback !== null) {
                    return $fallback();
                }

                throw $e;
            }
        });
    }

    /**
     * Execute with circuit breaker support
     *
     * @template T
     * @param callable(): T $operation
     * @param callable(): T|null $fallback
     * @return AsyncPromise<T>
     */
    public function executeWithCircuitBreaker(callable $operation, ?callable $fallback = null): AsyncPromise
    {
        if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
            if ($this->circuitBreakerManager->isCircuitOpen($this->boundaryName, $this->config)) {
                $this->log('info', 'Circuit breaker is open, using fallback');

                if ($fallback !== null) {
                    return AsyncPromise::resolve($fallback());
                }

                return AsyncPromise::reject(new AsyncCircuitBreakerOpenException(
                    "Circuit breaker is open for boundary '{$this->boundaryName}'"
                ));
            }
        }

        return AsyncPromise::create(function () use ($operation, $fallback) {
            try {
                $result = $operation();

                if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
                    $this->circuitBreakerManager->recordSuccess($this->boundaryName, $this->config);
                }

                return $result;

            } catch (Throwable $e) {
                if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
                    $this->circuitBreakerManager->recordFailure($this->boundaryName, $this->config);
                }

                return $this->handleFailureAsync($e, $fallback);
            }
        });
    }

    /**
     * Execute batch operations with individual error boundaries
     *
     * @template T
     * @param array<T> $items
     * @param callable(T): mixed $processor
     * @param int $maxConcurrency
     * @return AsyncPromise<AsyncBulkResult>
     */
    public function executeBatch(
        array $items,
        callable $processor,
        int $maxConcurrency = 10
    ): AsyncPromise {
        return AsyncPromise::create(function () use ($items, $processor, $maxConcurrency) {
            $operations = [];

            foreach ($items as $key => $item) {
                $operations[$key] = fn () => $processor($item);
            }

            $results = $this->fiberManager->throttled($operations, $maxConcurrency);

            $successful = [];
            $errors = [];

            foreach ($results as $key => $result) {
                if ($result instanceof Throwable) {
                    $errors[$key] = $result;
                } else {
                    $successful[$key] = $result;
                }
            }

            return new AsyncBulkResult(
                results: $successful,
                errors: $errors,
                totalOperations: count($items),
                successfulOperations: count($successful),
            );
        });
    }

    /**
     * Execute operation with retry using framework components
     */
    private function executeWithRetryAsync(callable $operation): mixed
    {
        $lastException = null;

        for ($attempt = 1; $attempt <= $this->config->maxRetries; $attempt++) {
            try {
                return $operation();
            } catch (Throwable $e) {
                $lastException = $e;

                // Don't retry certain types of errors
                if (! $this->shouldRetry($e)) {
                    break;
                }

                if ($attempt < $this->config->maxRetries) {
                    $delay = $this->calculateRetryDelay($attempt);
                    $this->log('info', "Async attempt {$attempt} failed, retrying in {$delay}ms", [
                        'exception' => $e->getMessage(),
                        'attempt' => $attempt,
                        'delay' => $delay,
                    ]);

                    // Use framework's timer for async sleep
                    $this->timer->sleep(Duration::fromMilliseconds($delay));
                }
            }
        }

        throw $lastException;
    }

    /**
     * Handle failure with async fallback
     */
    private function handleFailureAsync(Throwable $exception, ?callable $fallback): mixed
    {
        $this->logFailure($exception, 'Async operation failed');

        if ($fallback === null) {
            throw $exception;
        }

        try {
            $result = $fallback();

            // Publish fallback executed event
            $this->publishEvent(new BoundaryFallbackExecuted(
                boundaryName: $this->boundaryName,
                originalException: $exception,
                fallbackReason: 'Async operation failed: ' . $exception->getMessage(),
                message: 'Async fallback executed successfully',
            ));

            return $result;

        } catch (Throwable $fallbackException) {
            $this->log('error', 'Async fallback also failed', [
                'original_exception' => $exception->getMessage(),
                'fallback_exception' => $fallbackException->getMessage(),
            ]);

            throw new AsyncBoundaryFailedException(
                "Both async operation and fallback failed in boundary '{$this->boundaryName}'",
                $this->boundaryName,
                $exception,
                $fallbackException
            );
        }
    }

    /**
     * Check if exception should trigger retry
     */
    private function shouldRetry(Throwable $e): bool
    {
        // Don't retry async-specific errors
        if ($e instanceof AsyncCircuitBreakerOpenException) {
            return false;
        }

        // Use same retry logic as synchronous boundary
        return true; // Simplified for this example
    }

    /**
     * Calculate retry delay
     */
    private function calculateRetryDelay(int $attempt): int
    {
        $baseMs = (int) $this->config->baseDelay->toMilliseconds();
        $maxMs = (int) $this->config->maxDelay->toMilliseconds();

        return match ($this->config->retryStrategy) {
            default => min($baseMs * $attempt, $maxMs),
        };
    }

    /**
     * Get async circuit breaker health
     */
    public function getCircuitHealthAsync(): AsyncPromise
    {
        return AsyncPromise::create(function () {
            if (! $this->config->circuitBreakerEnabled || ! $this->circuitBreakerManager) {
                return null;
            }

            return $this->circuitBreakerManager->getCircuitHealth($this->boundaryName);
        });
    }

    /**
     * Reset circuit breaker asynchronously
     */
    public function resetCircuitAsync(): AsyncPromise
    {
        return AsyncPromise::create(function () {
            if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
                $this->circuitBreakerManager->resetCircuit($this->boundaryName);
            }
        });
    }

    private function logFailure(Throwable $e, string $message): void
    {
        $this->log('error', $message, [
            'exception' => $e::class,
            'message' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
        ]);
    }

    private function log(string $level, string $message, array $context = []): void
    {
        if ($this->logger) {
            $context['boundary'] = $this->boundaryName;
            $context['async'] = true;

            match ($level) {
                'debug' => $this->logger->debug("[AsyncBoundary] {$message}", $context),
                'info' => $this->logger->info("[AsyncBoundary] {$message}", $context),
                'warning' => $this->logger->warning("[AsyncBoundary] {$message}", $context),
                'error' => $this->logger->error("[AsyncBoundary] {$message}", $context),
                default => $this->logger->info("[AsyncBoundary] {$message}", $context),
            };
        }
    }

    private function publishEvent($event): void
    {
        if ($this->eventPublisher !== null) {
            $this->eventPublisher->publish($event);
        }
    }
}
