<?php

declare(strict_types=1);

namespace App\Framework\Redis;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Serializer\Json\JsonSerializer;
use DateTimeInterface;

/**
 * High-level Key-Value Store API for Redis
 * Provides simplified, type-safe operations with automatic JSON serialization
 */
final readonly class RedisKvStore
{
    public function __construct(
        private RedisConnectionInterface $connection,
        private JsonSerializer $serializer,
        private ?string $keyPrefix = null
    ) {
    }

    /**
     * Get value for given key
     * Returns null if key doesn't exist
     * Automatically deserializes JSON values
     */
    public function get(string $key): mixed
    {
        $fullKey = $this->applyPrefix($key);
        $value = $this->connection->get($fullKey);

        if ($value === false) {
            return null;
        }

        return $this->deserialize($value);
    }

    /**
     * Set key-value pair with optional expiration
     *
     * @param string $key The key to set
     * @param mixed $value The value (will be JSON encoded if array/object)
     * @param Duration|DateTimeInterface|null $expiration Optional expiration time
     * @return bool True on success
     * @throws InvalidRedisTtlException If TTL is invalid
     */
    public function set(
        string $key,
        mixed $value,
        Duration|DateTimeInterface|null $expiration = null
    ): bool {
        $fullKey = $this->applyPrefix($key);
        $serialized = $this->serialize($value);

        if ($expiration === null) {
            return $this->connection->set($fullKey, $serialized);
        }

        $ttl = $this->resolveTtl($expiration);
        $this->validateTtl($ttl);

        return $this->connection->set($fullKey, $serialized, $ttl);
    }

    /**
     * Delete one or more keys
     *
     * @param string ...$keys Keys to delete
     * @return int Number of keys deleted
     */
    public function delete(string ...$keys): int
    {
        if (empty($keys)) {
            return 0;
        }

        $fullKeys = array_map(fn ($k) => $this->applyPrefix($k), $keys);

        return $this->connection->delete(...$fullKeys);
    }

    /**
     * Check if key exists
     */
    public function exists(string ...$keys): int
    {
        if (empty($keys)) {
            return 0;
        }

        $fullKeys = array_map(fn ($k) => $this->applyPrefix($k), $keys);

        return $this->connection->exists(...$fullKeys);
    }

    /**
     * Get TTL for key in seconds
     * Returns -1 if key has no expiration
     * Returns -2 if key doesn't exist
     */
    public function ttl(string $key): int
    {
        $fullKey = $this->applyPrefix($key);

        return $this->connection->ttl($fullKey);
    }

    /**
     * Set expiration on existing key
     *
     * @throws InvalidRedisTtlException If TTL is invalid
     */
    public function expire(string $key, Duration|DateTimeInterface $expiration): bool
    {
        $fullKey = $this->applyPrefix($key);
        $ttl = $this->resolveTtl($expiration);
        $this->validateTtl($ttl);

        return $this->connection->expire($fullKey, $ttl);
    }

    /**
     * Increment numeric value by amount
     */
    public function increment(string $key, int $by = 1): int
    {
        $fullKey = $this->applyPrefix($key);

        return $this->connection->incr($fullKey, $by);
    }

    /**
     * Decrement numeric value by amount
     */
    public function decrement(string $key, int $by = 1): int
    {
        $fullKey = $this->applyPrefix($key);

        return $this->connection->decr($fullKey, $by);
    }

    /**
     * Get multiple values at once
     *
     * @param string ...$keys
     * @return array<string, mixed> Array of key => value pairs (null for missing keys)
     */
    public function getMultiple(string ...$keys): array
    {
        if (empty($keys)) {
            return [];
        }

        $fullKeys = array_map(fn ($k) => $this->applyPrefix($k), $keys);
        $values = $this->connection->mGet($fullKeys);

        $result = [];
        foreach ($keys as $index => $originalKey) {
            $value = $values[$index] ?? false;
            $result[$originalKey] = $value === false ? null : $this->deserialize($value);
        }

        return $result;
    }

    /**
     * Set multiple key-value pairs at once
     *
     * @param array<string, mixed> $keyValuePairs
     */
    public function setMultiple(array $keyValuePairs): bool
    {
        if (empty($keyValuePairs)) {
            return true;
        }

        $prefixedPairs = [];
        foreach ($keyValuePairs as $key => $value) {
            $prefixedPairs[$this->applyPrefix($key)] = $this->serialize($value);
        }

        return $this->connection->mSet($prefixedPairs);
    }

    /**
     * Flush all keys in current database
     * WARNING: Destructive operation!
     */
    public function flush(): void
    {
        $this->connection->flush();
    }

    /**
     * Apply key prefix if configured
     */
    private function applyPrefix(string $key): string
    {
        return $this->keyPrefix !== null
            ? $this->keyPrefix . $key
            : $key;
    }

    /**
     * Serialize value for storage using JsonSerializer
     * Handles primitives and complex types
     */
    private function serialize(mixed $value): string
    {
        // Primitives can be stored directly without JSON encoding
        if (is_string($value)) {
            return $value;
        }

        if (is_numeric($value) || is_bool($value)) {
            return (string) $value;
        }

        // Use framework's JsonSerializer for arrays and objects
        // It includes memory protection and proper error handling
        return $this->serializer->serialize($value);
    }

    /**
     * Deserialize value from storage using JsonSerializer
     * Attempts JSON decode, falls back to string
     */
    private function deserialize(string $value): mixed
    {
        // Try JSON decode using framework's serializer
        // It handles large data and provides better error messages
        try {
            return $this->serializer->deserialize($value);
        } catch (\App\Framework\Serializer\Exception\DeserializeException) {
            // Not JSON or deserialization failed, return as-is
            return $value;
        }
    }

    /**
     * Resolve TTL from Duration or DateTimeInterface
     */
    private function resolveTtl(Duration|DateTimeInterface $expiration): int
    {
        return match (true) {
            $expiration instanceof Duration => $expiration->toSeconds(),
            $expiration instanceof DateTimeInterface => max(0, $expiration->getTimestamp() - time()),
            default => throw new \InvalidArgumentException('Invalid expiration type')
        };
    }

    /**
     * Validate TTL is positive
     *
     * @throws InvalidRedisTtlException
     */
    private function validateTtl(int $ttl): void
    {
        if ($ttl < 0) {
            throw InvalidRedisTtlException::negative($ttl);
        }

        if ($ttl === 0) {
            throw InvalidRedisTtlException::zero();
        }
    }
}
