<?php

declare(strict_types=1);

namespace App\Framework\StateManagement;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;

/**
 * In-memory implementation of state manager (for testing and simple use cases)
 *
 * @template T of SerializableState
 * @implements StateManager<T>
 */
final class InMemoryStateManager implements StateManager
{
    private array $states = [];

    private array $expirationTimes = [];

    private int $hitCount = 0;

    private int $missCount = 0;

    private int $setCount = 0;

    private int $removeCount = 0;

    private int $updateCount = 0;

    private Duration $totalSetTime;

    private Duration $totalGetTime;

    public function __construct(
        private readonly string $stateClass,
    ) {
        $this->totalSetTime = Duration::zero();
        $this->totalGetTime = Duration::zero();
    }

    public function getState(string $key): mixed
    {
        $startTime = Timestamp::now();

        try {
            // Check if key exists and is not expired
            if (! $this->isValidKey($key)) {
                $this->missCount++;

                return null;
            }

            $data = $this->states[$key];

            /** @var T $state */
            $state = call_user_func([$this->stateClass, 'fromArray'], $data);
            $this->hitCount++;

            return $state;
        } finally {
            $elapsed = Timestamp::now()->diff($startTime);
            $this->totalGetTime = $this->totalGetTime->add($elapsed);
        }
    }

    public function setState(string $key, mixed $state, ?Duration $ttl = null): void
    {
        $startTime = Timestamp::now();

        try {
            if (! $state instanceof SerializableState) {
                throw new \InvalidArgumentException("State must implement SerializableState interface");
            }

            $this->states[$key] = $state->toArray();

            // Set expiration time if TTL provided
            if ($ttl !== null && ! $ttl->isZero()) {
                $this->expirationTimes[$key] = Timestamp::now()->add($ttl)->toFloat();
            } else {
                unset($this->expirationTimes[$key]);
            }

            $this->setCount++;
        } finally {
            $elapsed = Timestamp::now()->diff($startTime);
            $this->totalSetTime = $this->totalSetTime->add($elapsed);
        }
    }

    public function hasState(string $key): bool
    {
        return $this->isValidKey($key);
    }

    public function removeState(string $key): void
    {
        unset($this->states[$key], $this->expirationTimes[$key]);
        $this->removeCount++;
    }

    public function updateState(string $key, callable $updater, ?Duration $ttl = null): mixed
    {
        $this->updateCount++;

        // Get current state
        $currentState = $this->getState($key);

        // Apply update function
        $newState = $updater($currentState);

        // Save updated state
        $this->setState($key, $newState, $ttl);

        return $newState;
    }

    public function getAllStates(): array
    {
        $validStates = [];

        foreach ($this->states as $key => $data) {
            if ($this->isValidKey($key)) {
                /** @var T $state */
                $state = call_user_func([$this->stateClass, 'fromArray'], $data);
                $validStates[$key] = $state;
            }
        }

        return $validStates;
    }

    public function clearAll(): void
    {
        $this->states = [];
        $this->expirationTimes = [];
    }

    public function getStatistics(): StateManagerStatistics
    {
        $this->cleanupExpiredKeys(); // Clean up before calculating stats

        $totalGets = $this->hitCount + $this->missCount;
        $averageGetTime = $totalGets > 0
            ? Duration::fromMilliseconds($this->totalGetTime->toMilliseconds() / $totalGets)
            : Duration::zero();

        $averageSetTime = $this->setCount > 0
            ? Duration::fromMilliseconds($this->totalSetTime->toMilliseconds() / $this->setCount)
            : Duration::zero();

        // Estimate memory usage
        $memoryUsage = Byte::fromBytes(
            strlen(serialize($this->states)) + strlen(serialize($this->expirationTimes))
        );

        return new StateManagerStatistics(
            totalKeys: count($this->states),
            hitCount: $this->hitCount,
            missCount: $this->missCount,
            setCount: $this->setCount,
            removeCount: $this->removeCount,
            updateCount: $this->updateCount,
            averageSetTime: $averageSetTime,
            averageGetTime: $averageGetTime,
            expiredKeys: 0, // We clean up expired keys immediately
            memoryUsage: $memoryUsage,
        );
    }

    /**
     * Create state manager for specific state type
     *
     * @template U of SerializableState
     * @param class-string<U> $stateClass
     * @return InMemoryStateManager<U>
     */
    public static function for(string $stateClass): self
    {
        return new self($stateClass);
    }

    private function isValidKey(string $key): bool
    {
        if (! isset($this->states[$key])) {
            return false;
        }

        // Check expiration
        if (isset($this->expirationTimes[$key])) {
            $now = Timestamp::now()->toFloat();
            if ($now > $this->expirationTimes[$key]) {
                $this->removeState($key);

                return false;
            }
        }

        return true;
    }

    private function cleanupExpiredKeys(): void
    {
        $now = Timestamp::now()->toFloat();

        foreach ($this->expirationTimes as $key => $expirationTime) {
            if ($now > $expirationTime) {
                $this->removeState($key);
            }
        }
    }
}
