<?php

namespace classes;

use Throwable;

class order_bookings_counts_cache
{
    public const PREFIX = 'order_bookings:counts:v1:';

    /**
     * Optional runtime adapter for tests.
     */
    private static ?object $adapter = null;

    public static function setAdapterForTests(?object $adapter): void
    {
        self::$adapter = $adapter;
    }

    public static function getTtl(): int
    {
        $raw = getenv('ORDER_BOOKINGS_COUNTS_CACHE_TTL');
        if ($raw === false || trim((string)$raw) === '') {
            return 30;
        }

        return max(0, (int)$raw);
    }

    public static function buildKey(array $context): string
    {
        $normalized = self::normalizeValue($context);
        $encoded = json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            $encoded = serialize($normalized);
        }

        return self::PREFIX . md5($encoded);
    }

    public static function getCounts(string $key): ?array
    {
        $raw = self::redisGet($key);
        if ($raw === null || $raw === '') {
            return null;
        }

        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return null;
        }

        return self::normalizeCounts($decoded);
    }

    public static function storeCounts(string $key, array $counts, ?int $ttl = null): void
    {
        $cacheTtl = $ttl ?? self::getTtl();
        if ($cacheTtl <= 0) {
            return;
        }

        $normalized = self::normalizeCounts($counts);
        if ($normalized === null) {
            return;
        }

        $encoded = json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($encoded)) {
            return;
        }

        self::redisSetEx($key, $encoded, $cacheTtl);
    }

    public static function clearAll(): void
    {
        self::clearPattern(self::PREFIX . '*');
    }

    private static function normalizeCounts(array $counts): ?array
    {
        if (!array_key_exists('past', $counts) || !array_key_exists('current', $counts) || !array_key_exists('future', $counts)) {
            return null;
        }

        return [
            'past' => max(0, (int)$counts['past']),
            'current' => max(0, (int)$counts['current']),
            'future' => max(0, (int)$counts['future']),
        ];
    }

    private static function normalizeValue(mixed $value): mixed
    {
        if (!is_array($value)) {
            return self::normalizeScalar($value);
        }

        $normalized = array_map([self::class, 'normalizeValue'], $value);

        if (array_is_list($normalized)) {
            if (self::isScalarList($normalized)) {
                sort($normalized);
            }
            return $normalized;
        }

        ksort($normalized);
        return $normalized;
    }

    private static function normalizeScalar(mixed $value): mixed
    {
        if (!is_string($value)) {
            return $value;
        }

        if (preg_match('/^-?\d+$/', $value) === 1) {
            return (int)$value;
        }

        if (is_numeric($value)) {
            return (float)$value;
        }

        return $value;
    }

    private static function isScalarList(array $value): bool
    {
        foreach ($value as $item) {
            if (is_array($item) || is_object($item)) {
                return false;
            }
        }

        return true;
    }

    private static function clearPattern(string $pattern): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->clear_keys($pattern);
        } catch (Throwable) {
            // Cache invalidation must never break request flow.
        }
    }

    private static function redisSetEx(string $key, string $value, int $ttl): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->setEx($key, $value, $ttl);
        } catch (Throwable) {
            // Best-effort cache write.
        }
    }

    private static function redisGet(string $key): ?string
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return null;
            }
            $value = $client->get($key);
            return is_string($value) ? $value : null;
        } catch (Throwable) {
            return null;
        }
    }

    private static function redisClient(): ?object
    {
        if (self::$adapter !== null) {
            return self::$adapter;
        }

        try {
            if (defined('redis')) {
                $instance = constant('redis');
                if (is_object($instance)) {
                    return $instance;
                }
            }

            return (new redis())->connect();
        } catch (Throwable) {
            return null;
        }
    }
}
