<?php

declare(strict_types=1);

namespace App\Framework\StateManagement;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Logging\Logger;

/**
 * Cache-based implementation of state manager
 *
 * @template T of SerializableState
 * @implements StateManager<T>
 */
final class CacheBasedStateManager implements StateManager
{
    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;

    /** @var array<StateTransformer> */
    private array $transformers = [];

    private bool $transformersSorted = false;

    public function __construct(
        private readonly Cache $cache,
        private readonly string $keyPrefix,
        private readonly string $stateClass,
        private readonly ?Duration $defaultTtl = null,
        private readonly ?Logger $logger = null,
        private readonly string $namespace = 'default',
    ) {
        $this->totalSetTime = Duration::zero();
        $this->totalGetTime = Duration::zero();
    }

    /**
     * Add a transformer to the pipeline
     *
     * Transformers are executed in priority order (high to low) on write,
     * and in reverse order on read.
     *
     * @param StateTransformer $transformer The transformer to add
     * @return self For method chaining
     */
    public function addTransformer(StateTransformer $transformer): self
    {
        $this->transformers[] = $transformer;
        $this->transformersSorted = false;

        $this->log('debug', 'Transformer added', [
            'transformer' => $transformer->getName(),
            'priority' => $transformer->getPriority()
        ]);

        return $this;
    }

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

        try {
            $cacheKey = $this->getCacheKey($key);
            $cacheItem = $this->cache->get($cacheKey);

            if (! $cacheItem->isHit) {
                $this->missCount++;
                $this->log('debug', "State miss for key: {$key}");

                return null;
            }

            $data = $cacheItem->value;

            // Apply transformOut pipeline (reverse order) if transformers are registered
            if (! empty($this->transformers)) {
                $state = $this->applyTransformOutPipeline($data);
            } else {
                // Legacy path: no transformers, expect array data
                if (! is_array($data)) {
                    $this->missCount++;
                    $this->log('warning', "Invalid state data for key: {$key}");

                    return null;
                }

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

            $this->hitCount++;
            $this->log('debug', "State hit for key: {$key}");

            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");
            }

            $cacheKey = $this->getCacheKey($key);
            $effectiveTtl = $ttl ?? $this->defaultTtl ?? Duration::fromHours(1);

            // Apply transformIn pipeline if transformers are registered
            if (! empty($this->transformers)) {
                $data = $this->applyTransformInPipeline($state);
            } else {
                // Legacy path: no transformers, store as array
                $data = $state->toArray();
            }

            $this->cache->set($cacheKey, $data, $effectiveTtl);
            $this->setCount++;
            $this->log('debug', "State set for key: {$key}", ['ttl' => $effectiveTtl->toSeconds()]);
        } finally {
            $elapsed = Timestamp::now()->diff($startTime);
            $this->totalSetTime = $this->totalSetTime->add($elapsed);
        }
    }

    public function hasState(string $key): bool
    {
        $cacheKey = $this->getCacheKey($key);
        $cacheItem = $this->cache->get($cacheKey);

        return $cacheItem->isHit;
    }

    public function removeState(string $key): void
    {
        $cacheKey = $this->getCacheKey($key);
        $this->cache->forget($cacheKey);
        $this->removeCount++;
        $this->log('debug', "State removed for key: {$key}");
    }

    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);

        $this->log('debug', "State updated for key: {$key}");

        return $newState;
    }

    public function getAllStates(): array
    {
        // Note: This would require cache enumeration support
        // For now, return empty array - could be enhanced with cache tag support
        $this->log('warning', "getAllStates() not supported by cache-based implementation");

        return [];
    }

    public function clearAll(): void
    {
        // Note: This would require cache enumeration or tag support
        // For now, we'll just log - could be enhanced with cache prefix clearing
        $this->log('warning', "clearAll() not fully supported by cache-based implementation");
    }

    public function getStatistics(): StateManagerStatistics
    {
        $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();

        return new StateManagerStatistics(
            totalKeys: 0, // Would require cache enumeration
            hitCount: $this->hitCount,
            missCount: $this->missCount,
            setCount: $this->setCount,
            removeCount: $this->removeCount,
            updateCount: $this->updateCount,
            averageSetTime: $averageSetTime,
            averageGetTime: $averageGetTime,
            expiredKeys: 0, // Would require cache inspection
            memoryUsage: Byte::fromBytes(0), // Would require cache size information
        );
    }

    /**
     * Create state manager for specific state type
     *
     * @template U of SerializableState
     * @param class-string<U> $stateClass
     * @return CacheBasedStateManager<U>
     */
    public static function for(
        Cache $cache,
        string $keyPrefix,
        string $stateClass,
        ?Duration $defaultTtl = null,
        ?Logger $logger = null,
        string $namespace = 'default',
    ): self {
        return new self($cache, $keyPrefix, $stateClass, $defaultTtl, $logger, $namespace);
    }

    private function getCacheKey(string $key): CacheKey
    {
        $fullKey = "{$this->keyPrefix}:{$this->namespace}:{$key}";

        return CacheKey::from($fullKey);
    }

    private function log(string $level, string $message, array $context = []): void
    {
        if ($this->logger === null) {
            return;
        }

        $context['namespace'] = $this->namespace;
        $context['key_prefix'] = $this->keyPrefix;
        $context['state_class'] = $this->stateClass;

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

    /**
     * Apply transformIn pipeline to state before storage
     *
     * Transformers execute in priority order (high to low).
     *
     * @param SerializableState $state The state to transform
     * @return mixed The transformed data (final transformer determines type)
     */
    private function applyTransformInPipeline(SerializableState $state): mixed
    {
        $this->ensureTransformersSorted();

        $data = $state;

        foreach ($this->transformers as $transformer) {
            $this->log('debug', "Applying transformIn: {$transformer->getName()}");
            $data = $transformer->transformIn($data);
        }

        return $data;
    }

    /**
     * Apply transformOut pipeline to data after retrieval
     *
     * Transformers execute in REVERSE priority order (low to high).
     *
     * @param mixed $data The stored data to transform
     * @return SerializableState The restored state object
     */
    private function applyTransformOutPipeline(mixed $data): SerializableState
    {
        $this->ensureTransformersSorted();

        // Reverse order for transformOut
        $transformers = array_reverse($this->transformers);

        foreach ($transformers as $transformer) {
            $this->log('debug', "Applying transformOut: {$transformer->getName()}");
            $data = $transformer->transformOut($data);
        }

        if (! $data instanceof SerializableState) {
            throw new \RuntimeException(
                'Transform pipeline must return SerializableState. ' .
                'Got: ' . get_debug_type($data)
            );
        }

        return $data;
    }

    /**
     * Ensure transformers are sorted by priority (high to low)
     *
     * Uses stable sort to preserve registration order for same priority.
     */
    private function ensureTransformersSorted(): void
    {
        if ($this->transformersSorted) {
            return;
        }

        usort($this->transformers, function (StateTransformer $a, StateTransformer $b): int {
            return $b->getPriority() <=> $a->getPriority(); // High to low
        });

        $this->transformersSorted = true;

        $this->log('debug', 'Transformers sorted by priority', [
            'count' => count($this->transformers),
            'order' => array_map(
                fn(StateTransformer $t) => $t->getName() . ':' . $t->getPriority(),
                $this->transformers
            )
        ]);
    }
}
