<?php

declare(strict_types=1);

namespace App\Framework\Async;

/**
 * Promise für async/await-ähnliche Programmierung
 */
final class AsyncPromise
{
    private mixed $result = null;

    private ?\Throwable $exception = null;

    private bool $resolved = false;

    /** @var array<callable> */
    private array $thenCallbacks = [];

    /** @var array<callable> */
    private array $catchCallbacks = [];

    /** @var array<callable> */
    private array $finallyCallbacks = [];

    public function __construct(
        private readonly FiberManager $fiberManager
    ) {
    }

    /**
     * Erstellt ein resolved Promise
     */
    public static function resolve(mixed $value, FiberManager $fiberManager): self
    {
        $promise = new self($fiberManager);
        $promise->result = $value;
        $promise->resolved = true;

        return $promise;
    }

    /**
     * Erstellt ein rejected Promise
     */
    public static function reject(\Throwable $exception, FiberManager $fiberManager): self
    {
        $promise = new self($fiberManager);
        $promise->exception = $exception;
        $promise->resolved = true;

        return $promise;
    }

    /**
     * Erstellt Promise aus Callable
     */
    public static function create(callable $executor, FiberManager $fiberManager): self
    {
        $promise = new self($fiberManager);

        $promise->fiberManager->async(function () use ($promise, $executor) {
            try {
                $result = $executor();
                $promise->doResolve($result);
            } catch (\Throwable $e) {
                $promise->doReject($e);
            }
        });

        return $promise;
    }

    /**
     * Wartet auf alle Promises
     */
    public static function all(array $promises, FiberManager $fiberManager): self
    {
        $allPromise = new self($fiberManager);

        $allPromise->fiberManager->async(function () use ($allPromise, $promises) {
            try {
                $results = [];
                foreach ($promises as $key => $promise) {
                    if ($promise instanceof self) {
                        $results[$key] = $promise->await();
                    } else {
                        $results[$key] = $promise;
                    }
                }
                $allPromise->doResolve($results);
            } catch (\Throwable $e) {
                $allPromise->doReject($e);
            }
        });

        return $allPromise;
    }

    /**
     * Wartet auf das erste erfolgreiche Promise
     */
    public static function race(array $promises, FiberManager $fiberManager): self
    {
        $racePromise = new self($fiberManager);

        foreach ($promises as $promise) {
            if ($promise instanceof self) {
                $promise->fiberManager->async(function () use ($racePromise, $promise) {
                    try {
                        if (! $racePromise->resolved) {
                            $result = $promise->await();
                            $racePromise->doResolve($result);
                        }
                    } catch (\Throwable $e) {
                        if (! $racePromise->resolved) {
                            $racePromise->doReject($e);
                        }
                    }
                });
            }
        }

        return $racePromise;
    }

    /**
     * Fügt Then-Handler hinzu
     */
    public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
    {
        $newPromise = new self($this->fiberManager);

        if ($this->resolved) {
            $this->executeThen($onFulfilled, $onRejected, $newPromise);
        } else {
            $this->thenCallbacks[] = [$onFulfilled, $onRejected, $newPromise];
        }

        return $newPromise;
    }

    /**
     * Fügt Catch-Handler hinzu
     */
    public function catch(callable $onRejected): self
    {
        return $this->then(null, $onRejected);
    }

    /**
     * Fügt Finally-Handler hinzu
     */
    public function finally(callable $callback): self
    {
        $newPromise = new self($this->fiberManager);

        if ($this->resolved) {
            $this->executeFinally($callback, $newPromise);
        } else {
            $this->finallyCallbacks[] = [$callback, $newPromise];
        }

        return $newPromise;
    }

    /**
     * Wartet synchron auf das Ergebnis
     */
    public function await(): mixed
    {
        while (! $this->resolved) {
            usleep(1000); // 1ms
        }

        if ($this->exception) {
            throw $this->exception;
        }

        return $this->result;
    }

    /**
     * Resolved das Promise
     */
    private function doResolve(mixed $value): void
    {
        if ($this->resolved) {
            return;
        }

        $this->result = $value;
        $this->resolved = true;

        foreach ($this->thenCallbacks as [$onFulfilled, $onRejected, $promise]) {
            $this->executeThen($onFulfilled, $onRejected, $promise);
        }

        foreach ($this->finallyCallbacks as [$callback, $promise]) {
            $this->executeFinally($callback, $promise);
        }
    }

    /**
     * Rejected das Promise
     */
    private function doReject(\Throwable $exception): void
    {
        if ($this->resolved) {
            return;
        }

        $this->exception = $exception;
        $this->resolved = true;

        foreach ($this->thenCallbacks as [$onFulfilled, $onRejected, $promise]) {
            $this->executeThen($onFulfilled, $onRejected, $promise);
        }

        foreach ($this->finallyCallbacks as [$callback, $promise]) {
            $this->executeFinally($callback, $promise);
        }
    }

    private function executeThen(?callable $onFulfilled, ?callable $onRejected, self $promise): void
    {
        $this->fiberManager->async(function () use ($onFulfilled, $onRejected, $promise) {
            try {
                if ($this->exception) {
                    if ($onRejected) {
                        $result = $onRejected($this->exception);
                        $promise->doResolve($result);
                    } else {
                        $promise->doReject($this->exception);
                    }
                } else {
                    if ($onFulfilled) {
                        $result = $onFulfilled($this->result);
                        $promise->doResolve($result);
                    } else {
                        $promise->doResolve($this->result);
                    }
                }
            } catch (\Throwable $e) {
                $promise->doReject($e);
            }
        });
    }

    private function executeFinally(callable $callback, self $promise): void
    {
        $this->fiberManager->async(function () use ($callback, $promise) {
            try {
                $callback();
                if ($this->exception) {
                    $promise->doReject($this->exception);
                } else {
                    $promise->doResolve($this->result);
                }
            } catch (\Throwable $e) {
                $promise->doReject($e);
            }
        });
    }

    /**
     * Prüft ob Promise resolved ist
     */
    public function isResolved(): bool
    {
        return $this->resolved;
    }

    /**
     * Gibt Promise-Statistiken zurück
     */
    public function getStats(): array
    {
        return [
            'resolved' => $this->resolved,
            'has_result' => $this->result !== null,
            'has_exception' => $this->exception !== null,
            'then_callbacks' => count($this->thenCallbacks),
            'catch_callbacks' => count($this->catchCallbacks),
            'finally_callbacks' => count($this->finallyCallbacks),
        ];
    }
}
