<?php

namespace classes;

use Throwable;

class system_search_cache
{
    public const PREFIX = 'system_search:v2:';
    public const QUERY_PREFIX = self::PREFIX . 'query:';
    public const DIRTY_TABLES_KEY = self::PREFIX . 'dirty_tables';
    public const REBUILD_REQUEST_KEY = self::PREFIX . 'rebuild_request';
    public const TABLE_VERSION_PREFIX = self::PREFIX . 'table_version:';
    /**
     * Optional runtime adapter for tests.
     */
    private static ?object $adapter = null;

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

    public static function getQuery(string $hash): ?array
    {
        $raw = self::redisGet(self::QUERY_PREFIX . $hash);
        if ($raw === null) {
            return null;
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return null;
        }
        return $decoded;
    }

    public static function setQuery(string $hash, array $payload, int $ttlSeconds = 120): void
    {
        self::redisSetEx(self::QUERY_PREFIX . $hash, json_encode($payload, JSON_UNESCAPED_UNICODE), $ttlSeconds);
    }

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

    public static function clearQueryCaches(): void
    {
        self::clearPattern(self::QUERY_PREFIX . '*');
    }

    public static function markDirtyTable(string $table): void
    {
        $table = trim($table, " `\t\n\r\0\x0B");
        if ($table === '') {
            return;
        }
        self::bumpTableVersion($table);
        $tables = self::redisGetArray(self::DIRTY_TABLES_KEY);
        if (!in_array($table, $tables, true)) {
            $tables[] = $table;
            self::redisSetArray(self::DIRTY_TABLES_KEY, $tables);
            // Keep dirty markers briefly in case cron is delayed.
            self::redisExpire(self::DIRTY_TABLES_KEY, 3600);
        }
    }

    public static function consumeDirtyTables(): array
    {
        $tables = self::redisGetArray(self::DIRTY_TABLES_KEY);
        self::redisDelete(self::DIRTY_TABLES_KEY);
        return $tables;
    }

    public static function peekDirtyTables(): array
    {
        return self::redisGetArray(self::DIRTY_TABLES_KEY);
    }

    public static function bumpTableVersion(string $table): int
    {
        $table = trim($table, " `\t\n\r\0\x0B");
        if ($table === '') {
            return 0;
        }

        $key = self::TABLE_VERSION_PREFIX . $table;
        try {
            $client = self::redisClient();
            if ($client === null) {
                return 0;
            }
            if (method_exists($client, 'incr')) {
                return (int)$client->incr($key);
            }
            $current = self::redisGet($key);
            $next = max(1, (int)$current + 1);
            self::redisSet($key, (string)$next);
            return $next;
        } catch (Throwable) {
            return 0;
        }
    }

    /**
     * @param array<int, string> $tables
     */
    public static function tableVersionFingerprint(array $tables): string
    {
        $versions = [];
        foreach ($tables as $table) {
            if (!is_string($table)) {
                continue;
            }
            $normalized = trim($table, " `\t\n\r\0\x0B");
            if ($normalized === '') {
                continue;
            }
            $versions[$normalized] = (int)(self::redisGet(self::TABLE_VERSION_PREFIX . $normalized) ?? 0);
        }
        ksort($versions);
        return md5(json_encode($versions, JSON_UNESCAPED_UNICODE));
    }

    public static function enqueueRebuild(string $scope = 'all', array $types = []): array
    {
        $payload = [
            'scope' => in_array($scope, ['all', 'types', 'dirty'], true) ? $scope : 'all',
            'types' => array_values(array_unique(array_filter(array_map('strval', $types)))),
            'requested_at' => time(),
        ];
        self::redisSet(self::REBUILD_REQUEST_KEY, json_encode($payload, JSON_UNESCAPED_UNICODE));
        self::redisExpire(self::REBUILD_REQUEST_KEY, 86400);
        return $payload;
    }

    public static function consumeRebuildRequest(): ?array
    {
        $raw = self::redisGet(self::REBUILD_REQUEST_KEY);
        if ($raw === null) {
            return null;
        }
        self::redisDelete(self::REBUILD_REQUEST_KEY);
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    private static function clearPattern(string $pattern): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->clear_keys($pattern);
        } catch (Throwable) {
            // Cache clear 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) {
        }
    }

    private static function redisSet(string $key, string $value): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->set($key, $value);
        } catch (Throwable) {
        }
    }

    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 redisDelete(string $key): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->delete($key);
        } catch (Throwable) {
        }
    }

    private static function redisSetArray(string $key, array $value): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->set_array($key, $value);
        } catch (Throwable) {
        }
    }

    private static function redisGetArray(string $key): array
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return [];
            }
            $value = $client->get_array($key);
            return is_array($value) ? $value : [];
        } catch (Throwable) {
            return [];
        }
    }

    private static function redisExpire(string $key, int $ttl): void
    {
        try {
            $client = self::redisClient();
            if ($client === null) {
                return;
            }
            $client->expire($key, $ttl);
        } catch (Throwable) {
        }
    }

    private static function redisClient(): ?object
    {
        if (self::$adapter !== null) {
            return self::$adapter;
        }
        if (!defined('redis')) {
            return null;
        }
        $client = constant('redis');
        return is_object($client) ? $client : null;
    }
}
