<?php

declare(strict_types=1);

namespace App\Framework\Async;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use Fiber;

/**
 * Timer-System für asynchrone zeitbasierte Operationen mit Value Objects
 */
final class AsyncTimer
{
    /** @var array<string, array{callback: callable, executeAt: Timestamp, type: string, interval?: Duration}> */
    private array $timers = [];

    /** @var array<string, array{callback: callable, interval: Duration, executeAt: Timestamp, type: string}> */
    private array $intervals = [];

    private bool $running = false;

    public function __construct(
        private readonly FiberManager $fiberManager,
        private readonly Clock $clock,
        private readonly Timer $timer
    ) {
    }

    /**
     * Führt eine Funktion nach einer Verzögerung aus
     * @deprecated Use setTimeoutDuration() instead
     */
    public function setTimeout(callable $callback, float $delaySeconds, ?string $id = null): string
    {
        return $this->setTimeoutDuration($callback, Duration::fromSeconds($delaySeconds), $id);
    }

    /**
     * Schedule a callback after a delay using Duration
     */
    public function setTimeoutDuration(callable $callback, Duration $delay, ?string $id = null): string
    {
        $id ??= uniqid('timeout_', true);
        $executeAt = $this->calculateExecuteTime($delay);

        $this->timers[$id] = [
            'callback' => $callback,
            'executeAt' => $executeAt,
            'type' => 'timeout',
        ];

        return $id;
    }

    /**
     * Führt eine Funktion wiederholt in Intervallen aus
     * @deprecated Use setIntervalDuration() instead
     */
    public function setInterval(callable $callback, float $intervalSeconds, ?string $id = null): string
    {
        return $this->setIntervalDuration($callback, Duration::fromSeconds($intervalSeconds), $id);
    }

    /**
     * Schedule a recurring callback using Duration
     */
    public function setIntervalDuration(callable $callback, Duration $interval, ?string $id = null): string
    {
        $id ??= uniqid('interval_', true);
        $executeAt = $this->calculateExecuteTime($interval);

        $this->intervals[$id] = [
            'callback' => $callback,
            'interval' => $interval,
            'executeAt' => $executeAt,
            'type' => 'interval',
        ];

        return $id;
    }

    /**
     * Entfernt einen Timer oder Interval
     */
    public function clear(string $id): bool
    {
        if (isset($this->timers[$id])) {
            unset($this->timers[$id]);

            return true;
        }

        if (isset($this->intervals[$id])) {
            unset($this->intervals[$id]);

            return true;
        }

        return false;
    }

    /**
     * Startet den Timer-Loop
     */
    public function start(): Fiber
    {
        return $this->fiberManager->async(function () {
            $this->running = true;

            while ($this->running) {
                $this->processTasks();
                $this->timer->sleep(Duration::fromMilliseconds(1)); // 1ms Auflösung
            }
        });
    }

    /**
     * Stoppt den Timer-Loop
     */
    public function stop(): void
    {
        $this->running = false;
    }

    /**
     * Wartet asynchron für eine bestimmte Zeit
     * @deprecated Use sleepDuration() instead
     */
    public function sleep(float $seconds): Fiber
    {
        return $this->sleepDuration(Duration::fromSeconds($seconds));
    }

    /**
     * Sleep for a duration
     */
    public function sleepDuration(Duration $duration): Fiber
    {
        return $this->fiberManager->async(function () use ($duration) {
            $endTime = $this->calculateExecuteTime($duration);

            while ($this->clock->time()->isBefore($endTime)) {
                $this->timer->sleep(Duration::fromMilliseconds(1));
            }
        });
    }

    /**
     * Wartet auf eine Bedingung mit Timeout
     * @deprecated Use waitForDuration() instead
     */
    public function waitFor(callable $condition, float $timeoutSeconds = 10, float $checkIntervalSeconds = 0.1): Fiber
    {
        return $this->waitForDuration(
            $condition,
            Duration::fromSeconds($timeoutSeconds),
            Duration::fromSeconds($checkIntervalSeconds)
        );
    }

    /**
     * Wait for a condition with timeout using Duration
     */
    public function waitForDuration(
        callable $condition,
        ?Duration $timeout = null,
        ?Duration $checkInterval = null
    ): Fiber {
        $timeout ??= Duration::fromSeconds(10);
        $checkInterval ??= Duration::fromMilliseconds(100);

        return $this->fiberManager->async(function () use ($condition, $timeout, $checkInterval) {
            $startTime = $this->clock->time();
            $endTime = $this->calculateExecuteTime($timeout);

            while ($this->clock->time()->isBefore($endTime)) {
                if ($condition()) {
                    return true;
                }
                $this->timer->sleep($checkInterval);
            }

            $elapsed = $startTime->age($this->clock);

            throw new AsyncTimeoutException(
                "Condition not met within {$elapsed->toHumanReadable()}"
            );
        });
    }

    private function processTasks(): void
    {
        $currentTime = $this->clock->time();

        // Verarbeite Timeouts
        foreach ($this->timers as $id => $timer) {
            if ($currentTime->isAfter($timer['executeAt']) || $currentTime->equals($timer['executeAt'])) {
                $this->fiberManager->async($timer['callback']);
                unset($this->timers[$id]);
            }
        }

        // Verarbeite Intervals
        foreach ($this->intervals as $id => &$interval) {
            if ($currentTime->isAfter($interval['executeAt']) || $currentTime->equals($interval['executeAt'])) {
                $this->fiberManager->async($interval['callback']);
                $interval['executeAt'] = $this->calculateExecuteTime($interval['interval']);
            }
        }
    }

    /**
     * Calculate execution time based on current time and delay
     */
    private function calculateExecuteTime(Duration $delay): Timestamp
    {
        $now = $this->clock->time();
        $futureTime = $now->toFloat() + $delay->toSeconds();

        return Timestamp::fromFloat($futureTime);
    }

    /**
     * Schedule a callback at a specific timestamp
     */
    public function scheduleAt(callable $callback, Timestamp $timestamp, ?string $id = null): string
    {
        $id ??= uniqid('scheduled_', true);

        $this->timers[$id] = [
            'callback' => $callback,
            'executeAt' => $timestamp,
            'type' => 'scheduled',
        ];

        return $id;
    }

    /**
     * Get next execution time for a timer
     */
    public function getNextExecution(string $id): ?Timestamp
    {
        if (isset($this->timers[$id])) {
            return $this->timers[$id]['executeAt'];
        }

        if (isset($this->intervals[$id])) {
            return $this->intervals[$id]['executeAt'];
        }

        return null;
    }

    /**
     * Get time until next execution
     */
    public function timeUntilExecution(string $id): ?Duration
    {
        $nextExecution = $this->getNextExecution($id);

        return $nextExecution?->timeUntil($this->clock);
    }

    /**
     * Gibt Timer-Statistiken zurück
     */
    public function getStats(): array
    {
        $nextExecution = $this->getNextExecutionTime();

        return [
            'running' => $this->running,
            'timeouts' => count($this->timers),
            'intervals' => count($this->intervals),
            'next_execution' => $nextExecution?->format('Y-m-d H:i:s.u'),
            'time_until_next' => $nextExecution?->timeUntil($this->clock)->toHumanReadable(),
        ];
    }

    /**
     * Get the next execution time across all timers
     */
    private function getNextExecutionTime(): ?Timestamp
    {
        $times = [];

        foreach ($this->timers as $timer) {
            $times[] = $timer['executeAt'];
        }

        foreach ($this->intervals as $interval) {
            $times[] = $interval['executeAt'];
        }

        if (empty($times)) {
            return null;
        }

        // Find earliest time
        usort($times, fn ($a, $b) => $a->isBefore($b) ? -1 : 1);

        return $times[0];
    }
}
