<?php
/**
 * Оптимізована система кешування
 * Оновлено для PHP 8.4+
 *
 * @package Flowaxy\Infrastructure\Cache
 * @version 1.0.0 Alpha prerelease
 * @requires PHP >= 8.4.0
 */

declare(strict_types=1);

namespace Flowaxy\Infrastructure\Cache;

use Error;
use Exception;
use Flowaxy\Contracts\Cache\CacheInterface;
use Flowaxy\Core\System\PathResolver;
use Flowaxy\Infrastructure\Cache\MultiLevelCache;
use Flowaxy\Support\Facades\Hooks;
use Flowaxy\Support\Facades\Log;
use Flowaxy\Support\Facades\Settings;
use Flowaxy\Support\Helpers\DatabaseHelper;
use PDO;
use Throwable;

use function class_exists;
use function file_exists;
use function is_dir;
use function is_file;
use function is_readable;
use function mt_rand;
use function register_shutdown_function;
use function rtrim;
use function time;
use const DS;

final class Cache implements CacheInterface
{
    /**
     * Безопасное логирование с проверкой доступности Log фасада
     */
    private static function logDebug(string $message, array $context = []): void
    {
        try {
            Log::debug($message, $context);
        } catch (Throwable $e) {
            // Ignore logging errors
        }
    }

    private static function logError(string $message, array $context = []): void
    {
        try {
            Log::error($message, $context);
        } catch (Throwable $e) {
            // Ignore logging errors
        }
    }

    private static function logWarning(string $message, array $context = []): void
    {
        try {
            Log::warning($message, $context);
        } catch (Throwable $e) {
            // Ignore logging errors
        }
    }

    private static function logInfo(string $message, array $context = []): void
    {
        try {
            Log::info($message, $context);
        } catch (Throwable $e) {
            // Ignore logging errors
        }
    }

    private static function logCritical(string $message, array $context = []): void
    {
        try {
            Log::critical($message, $context);
        } catch (Throwable $e) {
            // Ignore logging errors
        }
    }
    /** @phpstan-ignore-next-line */
    private static ?self $instance = null;
    private static bool $loadingSettings = false; // Прапорець для запобігання рекурсії
    private bool $settingsLoaded = false; // Прапорець завантаження налаштувань
    private string $cacheDir;
    private int $defaultTtl = 3600; // 1 година
    private array $memoryCache = []; // Кеш у пам'яті для поточного запиту
    private bool $enabled = true;
    private bool $autoCleanup = true;
    private int $compressionThreshold = 10240; // 10KB за замовчуванням
    /** @phpstan-ignore-next-line */
    private const CACHE_FILE_EXTENSION = '.cache';

    /**
     * MultiLevelCache драйвер (опціонально)
     */
    private ?object $multiLevelCache = null;
    private bool $useMultiLevelCache = false;

    /**
     * Конструктор (приватний для Singleton)
     */
    private function __construct()
    {
        $this->cacheDir = PathResolver::cache() . DS;
        $this->cacheDir = rtrim($this->cacheDir, '/\\') . DS;
        $this->ensureCacheDir();

        // Спробуємо ініціалізувати MultiLevelCache, якщо доступний
        $this->initMultiLevelCache();

        // НЕ завантажуємо налаштування в конструкторі, щоб уникнути циклічних залежностей
        // Налаштування будуть завантажені пізніше при першому зверненні або через reloadSettings()
    }

    /**
     * Ініціалізація MultiLevelCache
     */
    private function initMultiLevelCache(): void
    {
        // Перевіряємо, чи доступний MultiLevelCache
        $multiLevelCacheFile = __DIR__ . '/MultiLevelCache.php';
        if (!file_exists($multiLevelCacheFile)) {
            return;
        }

        if (class_exists(MultiLevelCache::class)) {
            try {
                $this->multiLevelCache = new MultiLevelCache();
                $this->useMultiLevelCache = true;
            } catch (Throwable $e) {
                // Якщо не вдалося ініціалізувати, використовуємо стару реалізацію
                $this->useMultiLevelCache = false;
            }
        }
    }

    /**
     * Завантаження налаштувань з SettingsManager
     *
     * @param bool $skipCleanup Пропустити автоматичне очищення (для уникнення циклічних залежностей)
     * @return void
     */
    private function loadSettings(bool $skipCleanup = false): void
    {
        // Запобігаємо рекурсії: якщо налаштування вже завантажуються, виходимо
        /** @phpstan-ignore-next-line */
        if (self::$loadingSettings) {
            return;
        }

        // Уникаємо циклічних залежностей: не завантажуємо налаштування, якщо SettingsManager ще не завантажено
        if (!class_exists('SettingsManager')) {
            // Використовуємо значення за замовчуванням
            return;
        }

        // Встановлюємо прапорець завантаження налаштувань
        /** @phpstan-ignore-next-line */
        self::$loadingSettings = true;

        try {
            // Завантажуємо налаштування через SettingsManager (тепер з файлів, немає рекурсії)
            $settings = Settings::manager();
            if ($settings !== null) {
                try {
                    $cacheEnabled = $settings->get('cache_enabled', '1');
                    if ($cacheEnabled === '' && ! $settings->has('cache_enabled')) {
                        $cacheEnabled = '1';
                    }
                    $this->enabled = $cacheEnabled === '1';

                    $cacheDefaultTtl = $settings->get('cache_default_ttl', '3600');
                    $this->defaultTtl = (int)$cacheDefaultTtl ?: 3600;

                    $cacheAutoCleanup = $settings->get('cache_auto_cleanup', '1');
                    if ($cacheAutoCleanup === '' && ! $settings->has('cache_auto_cleanup')) {
                        $cacheAutoCleanup = '1';
                    }
                    $this->autoCleanup = $cacheAutoCleanup === '1';
                } catch (Exception $e) {
                        self::logError('Cache::loadSettings DB error: ' . $e->getMessage(), ['exception' => $e]);
                    }

                // Виконуємо автоматичне очищення при необхідності (тільки якщо не в конструкторі)
                if (! $skipCleanup && $this->autoCleanup && mt_rand(1, 1000) <= 1) { // 0.1% шанс на очищення при кожному запиті
                    // Запускаємо очищення у фоні, щоб не блокувати запит
                    register_shutdown_function(function () {
                        $this->cleanup();
                    });
                }
            }
        } catch (Exception $e) {
            self::logError('Cache::loadSettings помилка: ' . $e->getMessage(), ['exception' => $e]);
        } catch (Error $e) {
            self::logCritical('Cache::loadSettings фатальна помилка: ' . $e->getMessage(), ['exception' => $e]);
        } finally {
            // Скидаємо прапорець завантаження налаштувань
            /** @phpstan-ignore-next-line */
            self::$loadingSettings = false;
        }
    }

    /**
     * Оновлення налаштувань (викликається після зміни налаштувань)
     *
     * @return void
     */
    public function reloadSettings(): void
    {
        // При оновленні налаштувань дозволяємо автоматичне очищення
        $this->loadSettings(false);
        $this->settingsLoaded = true;
    }

    /**
     * Отримання екземпляра класу (Singleton)
     *
     * @return self
     * @phpstan-ignore-next-line
     */
    public static function getInstance(): self
    {
        /** @phpstan-ignore-next-line */
        if (self::$instance === null) {
            /** @phpstan-ignore-next-line */
            self::$instance = new self();
        }

        /** @phpstan-ignore-next-line */
        return self::$instance;
    }

    /**
     * Створення директорії кешу
     *
     * @return void
     */
    private function ensureCacheDir(): void
    {
        // Создаем директорию, если её нет
        if (!is_dir($this->cacheDir)) {
            if (!@mkdir($this->cacheDir, 0755, true) && !is_dir($this->cacheDir)) {
                self::logWarning("Cache: Directory does not exist and could not be created", ['directory' => $this->cacheDir]);
                return;
            }
        }

        // Создаем .htaccess файл для защиты директории
        $this->ensureHtaccessFile($this->cacheDir);
    }

    /**
     * Создание .htaccess файла для защиты директории от прямого доступа
     *
     * @param string $dirPath Путь к директории
     * @return void
     */
    private function ensureHtaccessFile(string $dirPath): void
    {
        $htaccessPath = $dirPath . DS . '.htaccess';

        // Если .htaccess уже существует, не перезаписываем
        if (file_exists($htaccessPath)) {
            return;
        }

        // Содержимое .htaccess для запрета прямого доступа
        $htaccessContent = "Deny from all\n";

        try {
            $created = @file_put_contents($htaccessPath, $htaccessContent) !== false;

            if ($created && file_exists($htaccessPath)) {
                @chmod($htaccessPath, 0644);
            } else {
                self::logWarning('Cache: Failed to create .htaccess file', ['path' => $htaccessPath]);
            }
        } catch (Throwable $e) {
            self::logWarning('Cache: Error creating .htaccess', [
                'path' => $htaccessPath,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Отримання даних з кешу
     *
     * @param string $key Ключ кешу
     * @param mixed $default Значення за замовчуванням
     * @return mixed
     */
    public function get(string $key, mixed $default = null): mixed
    {
        // Ліниве завантаження налаштувань при першому використанні
        if (! $this->settingsLoaded) {
            $this->loadSettings(true);
            $this->settingsLoaded = true;
        }

        // Якщо кешування вимкнено, повертаємо значення за замовчуванням
        if (! $this->enabled) {
            return $default;
        }

        // Валідація ключа
        if (empty($key)) {
            self::logDebug('Cache::get: Empty key provided', ['default' => $default]);
            return $default;
        }

        self::logDebug('Cache::get: Retrieving from cache', ['key' => $key]);

        // Використовуємо MultiLevelCache, якщо доступний
        if ($this->useMultiLevelCache && $this->multiLevelCache !== null) {
            try {
                $result = $this->multiLevelCache->get($key, $default);
                if ($result !== $default) {
                    if (class_exists(Log::class)) {
                        self::logDebug('Cache::get: Retrieved from MultiLevelCache', ['key' => $key]);
                    }
                }
                return $result;
            } catch (Throwable $e) {
                self::logWarning('Cache::get: MultiLevelCache error, falling back', [
                    'key' => $key,
                    'error' => $e->getMessage(),
                ]);
                // Fallback до старої реалізації при помилці
            }
        }

        // Спочатку перевіряємо кеш у пам'яті
        if (isset($this->memoryCache[$key])) {
            self::logDebug('Cache::get: Retrieved from memory cache', ['key' => $key]);
            return $this->memoryCache[$key];
        }

        $filename = $this->getFilename($key);

        if (!file_exists($filename) || !is_readable($filename)) {
            return $default;
        }

        $data = @\file_get_contents($filename);
        if ($data === false) {
            return $default;
        }

        try {
            $cached = \unserialize($data, ['allowed_classes' => false]);

            // Перевіряємо структуру даних
            if (!\is_array($cached) || !isset($cached['expires']) || !isset($cached['data'])) {
                @\unlink($filename);

                return $default;
            }

            // Перевіряємо термін дії (0 = без обмеження)
            if ($cached['expires'] !== 0 && $cached['expires'] < \time()) {
                $this->delete($key);

                return $default;
            }

            // Розпаковуємо стиснуті дані
            $data = $cached['data'];
            if (\is_array($data) && isset($data['_compressed']) && $data['_compressed'] === true) {
                if (\function_exists('gzuncompress')) {
                    $compressed = \base64_decode($data['_data']);
                    $uncompressed = \gzuncompress($compressed);
                    if ($uncompressed !== false) {
                        $data = \unserialize($uncompressed, ['allowed_classes' => false]);
                    }
                }
            }

            // Зберігаємо в кеш пам'яті
            $this->memoryCache[$key] = $data;

            self::logDebug('Cache::get: Retrieved from file cache', ['key' => $key]);

            // Хук для плагінів (відстеження статистики і т.д.)
            try {
                Hooks::dispatch('cache_get', $key);
            } catch (Throwable $e) {
                // Ігноруємо помилки хуків
            }

            return $data;
        } catch (Exception $e) {
            self::logError("Cache помилка десеріалізації для ключа '{$key}': " . $e->getMessage(), ['key' => $key, 'exception' => $e]);
            @unlink($filename);

            return $default;
        }
    }

    /**
     * Збереження даних у кеш зі стисненням
     *
     * @param string $key Ключ кешу
     * @param mixed $data Дані для кешування
     * @param int|null $ttl Час життя в секундах
     * @param bool $compressed Примусово стиснути дані
     * @return bool
     */
    public function setCompressed(string $key, $data, ?int $ttl = null, bool $compressed = true): bool
    {
        if ($compressed && function_exists('gzcompress')) {
            $serialized = serialize($data);

            // Стискаємо тільки якщо дані досить великі
            if (\strlen($serialized) >= $this->compressionThreshold) {
                $compressed = \gzcompress($serialized, 6);
                if ($compressed !== false) {
                    $data = [
                        '_compressed' => true,
                        '_data' => \base64_encode($compressed),
                    ];
                }
            }
        }

        return $this->set($key, $data, $ttl);
    }

    /**
     * Збереження даних у кеш
     *
     * @param string $key Ключ кешу
     * @param mixed $data Дані для кешування
     * @param int|null $ttl Час життя в секундах
     * @return bool
     */
    public function set(string $key, mixed $data, ?int $ttl = null): bool
    {
        // Ліниве завантаження налаштувань при першому використанні
        if (! $this->settingsLoaded) {
            $this->loadSettings(true);
            $this->settingsLoaded = true;
        }

        // Якщо кешування вимкнено, не зберігаємо
        if (! $this->enabled) {
            return false;
        }

        // Валідація ключа
        if (empty($key)) {
            self::logWarning('Cache::set: Empty key provided');
            return false;
        }

        self::logDebug('Cache::set: Setting cache value', ['key' => $key, 'ttl' => $ttl]);

        if ($ttl === null) {
            $ttl = $this->defaultTtl;
        }

        // Валідація TTL
        if ($ttl < 0) {
            self::logWarning('Cache::set: Invalid TTL, using default', ['provided_ttl' => $ttl, 'default_ttl' => $this->defaultTtl]);
            $ttl = $this->defaultTtl;
        }

        // Автоматичне стиснення великих об'єктів
        $serializedData = \serialize($data);
        if (\strlen($serializedData) >= $this->compressionThreshold && \function_exists('gzcompress')) {
            $compressed = \gzcompress($serializedData, 6);
            if ($compressed !== false) {
                $data = [
                    '_compressed' => true,
                    '_data' => \base64_encode($compressed),
                ];
            }
        }

        $cached = [
            'data' => $data,
            'expires' => time() + $ttl,
            'created' => time(),
        ];

        try {
            $serialized = \serialize($cached);
        } catch (Exception $e) {
            self::logError("Cache помилка серіалізації для ключа '{$key}': " . $e->getMessage(), ['key' => $key, 'exception' => $e]);
            return false;
        }

        $filename = $this->getFilename($key);
        $result = @\file_put_contents($filename, $serialized, LOCK_EX);

        if ($result !== false) {
            // Встановлюємо права доступу (якщо можливо)
            // В WSL/Windows на NTFS файловій системі chmod може не працювати,
            // тому використовуємо безпечний спосіб без генерації warning
            if (file_exists($filename)) {
                $this->setPermissions($filename, 0644);
            }

            // Зберігаємо в кеш пам'яті
            $this->memoryCache[$key] = $data;

            self::logInfo('Cache::set: Cache value saved successfully', [
                'key' => $key,
                'ttl' => $ttl,
                'size' => \strlen($serialized),
            ]);

            // Хук для плагінів
            try {
                Hooks::dispatch('cache_set', $key, $ttl);
            } catch (Throwable $e) {
                // Ігноруємо помилки хуків
            }

            return true;
        }

        self::logError("Cache помилка запису для ключа '{$key}' у файл '{$filename}'", ['key' => $key, 'filename' => $filename]);

        return false;
    }

    /**
     * Видалення з кешу
     *
     * @param string $key Ключ кешу
     * @return bool
     */
    public function delete(string $key): bool
    {
        // Валідація ключа
        if (empty($key)) {
            self::logWarning('Cache::delete: Empty key provided');
            return false;
        }

        self::logDebug('Cache::delete: Deleting cache key', ['key' => $key]);

        // Використовуємо MultiLevelCache, якщо доступний
        if ($this->useMultiLevelCache && $this->multiLevelCache !== null) {
            try {
                $result = $this->multiLevelCache->delete($key);
                unset($this->memoryCache[$key]);
                if ($result) {
                    self::logInfo('Cache::delete: Deleted from MultiLevelCache', ['key' => $key]);
                }
                return $result;
            } catch (Throwable $e) {
                self::logWarning('Cache::delete: MultiLevelCache error, falling back', [
                    'key' => $key,
                    'error' => $e->getMessage(),
                ]);
                // Fallback до старої реалізації при помилці
            }
        }

        unset($this->memoryCache[$key]);

        $filename = $this->getFilename($key);
        if (\file_exists($filename)) {
            $result = @\unlink($filename);

            if ($result) {
                self::logInfo('Cache::delete: Cache key deleted successfully', ['key' => $key]);
            } elseif (!$result) {
                self::logWarning('Cache::delete: Failed to delete cache file', ['key' => $key, 'filename' => $filename]);
            }

            // Хук для плагінів
            if ($result) {
                try {
                    Hooks::dispatch('cache_delete', $key);
                } catch (Throwable $e) {
                    // Ігноруємо помилки хуків
                }
            }

            return $result;
        }

        self::logDebug('Cache::delete: Cache file does not exist', ['key' => $key]);

        return true;
    }

    /**
     * Перевірка існування ключа
     *
     * @param string $key Ключ кешу
     * @return bool
     */
    public function has(string $key): bool
    {
        // Ліниве завантаження налаштувань при першому використанні
        if (! $this->settingsLoaded) {
            $this->loadSettings(true);
            $this->settingsLoaded = true;
        }

        // Якщо кешування вимкнено, завжди повертаємо false
        if (! $this->enabled) {
            return false;
        }

        // Валідація ключа
        if (empty($key)) {
            self::logDebug('Cache::has: Empty key provided');
            return false;
        }

        self::logDebug('Cache::has: Checking cache key existence', ['key' => $key]);

        // Використовуємо MultiLevelCache, якщо доступний
        if ($this->useMultiLevelCache && $this->multiLevelCache !== null) {
            try {
                $result = $this->multiLevelCache->has($key);
                self::logDebug('Cache::has: Checked via MultiLevelCache', ['key' => $key, 'exists' => $result]);
                return $result;
            } catch (Throwable $e) {
                self::logWarning('Cache::has: MultiLevelCache error, falling back', [
                    'key' => $key,
                    'error' => $e->getMessage(),
                ]);
                // Fallback до старої реалізації при помилці
            }
        }

        if (isset($this->memoryCache[$key])) {
            self::logDebug('Cache::has: Key exists in memory cache', ['key' => $key]);
            return true;
        }

        $filename = $this->getFilename($key);

        if (!file_exists($filename) || !is_readable($filename)) {
            return false;
        }

        $data = @\file_get_contents($filename);
        if ($data === false) {
            return false;
        }

        try {
            $cached = \unserialize($data, ['allowed_classes' => false]);

            // Перевіряємо структуру даних
            if (!\is_array($cached) || !isset($cached['expires'])) {
                @\unlink($filename);

                return false;
            }

            // Перевіряємо термін дії
            if ($cached['expires'] < time()) {
                $this->delete($key);

                return false;
            }

            self::logDebug('Cache::has: Key exists in file cache', ['key' => $key]);
            return true;
        } catch (Exception $e) {
            self::logError("Cache помилка перевірки для ключа '{$key}': " . $e->getMessage(), ['key' => $key, 'exception' => $e]);
            @unlink($filename);

            return false;
        }
    }

    /**
     * Отримання або встановлення значення
     *
     * @param string $key Ключ кешу
     * @param callable $callback Функція для отримання даних
     * @param int|null $ttl Час життя в секундах
     * @return mixed
     */
    public function remember(string $key, callable $callback, ?int $ttl = null): mixed
    {
        // Ліниве завантаження налаштувань при першому використанні
        if (! $this->settingsLoaded) {
            $this->loadSettings(true);
            $this->settingsLoaded = true;
        }

        // Якщо кешування вимкнено, просто виконуємо callback
        if (! $this->enabled) {
            try {
                return $callback();
            } catch (Exception $e) {
                try {
                    Log::Error("Помилка callback remember для ключа '{$key}'", [
                        'exception' => $e,
                        'key' => $key,
                    ]);
                } catch (Throwable $logError) {
                    // Ignore logging errors
                }
                throw $e;
            }
        }

        $value = $this->get($key);

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

        try {
            $value = $callback();
            $this->set($key, $value, $ttl);

            return $value;
        } catch (Exception $e) {
            try {
                Log::Error("Помилка callback remember для ключа '{$key}'", [
                    'exception' => $e,
                    'key' => $key,
                ]);
            } catch (Throwable $logError) {
                // Ignore logging errors
            }
            throw $e;
        }
    }

    /**
     * Очищення всього кешу
     *
     * @return bool
     */
    public function clear(): bool
    {
        self::logDebug('Cache::clear: Starting cache clear operation', ['cache_dir' => $this->cacheDir]);

        $this->memoryCache = [];

        // Системні файли, які не потрібно видаляти
        $systemFiles = ['.gitkeep', '.htaccess'];

        // Рекурсивне сканування директорії кешу
        try {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($this->cacheDir, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::SELF_FIRST
            );

            $success = true;
            foreach ($iterator as $file) {
                if ($file->isFile()) {
                    $filename = $file->getFilename();

                    // Пропускаємо системні файли
                    if (\in_array($filename, $systemFiles)) {
                        continue;
                    }

                    // Видаляємо тільки файли кешу
                    /** @phpstan-ignore-next-line */
                    $extension = '.cache';
                    if (\pathinfo($filename, PATHINFO_EXTENSION) === \ltrim($extension, '.')) {
                        if (!@\unlink($file->getPathname())) {
                            $success = false;
                        }
                    }
                }
            }
        } catch (Exception $e) {
            self::logWarning('Cache::clear: Recursive scan failed, using fallback method', [
                'error' => $e->getMessage(),
                'exception' => $e,
            ]);
            // Якщо рекурсивне сканування не вдалося, використовуємо старий метод
            $extension = '.cache';
            $pattern = $this->cacheDir . '*' . $extension;
            $files = \glob($pattern);

            if ($files === false) {
                self::logError('Cache::clear: Failed to glob cache files', ['pattern' => $pattern]);
                return false;
            }

            $success = true;
            foreach ($files as $file) {
                if (\is_file($file)) {
                    $filename = \basename($file);
                    if (!\in_array($filename, $systemFiles)) {
                        if (!@\unlink($file)) {
                            $success = false;
                        }
                    }
                }
            }
        }

        if ($success) {
            self::logInfo('Cache::clear: Cache cleared successfully');
        } elseif (!$success) {
            self::logWarning('Cache::clear: Some cache files could not be deleted');
        }

        return $success;
    }

    /**
     * Очищення застарілого кешу
     *
     * @return int Кількість видалених файлів
     */
    public function cleanup(): int
    {
        self::logDebug('Cache::cleanup: Starting cache cleanup operation', ['cache_dir' => $this->cacheDir]);

        $cleaned = 0;

        // Системні файли, які не потрібно видаляти
        $systemFiles = ['.gitkeep', '.htaccess'];
        $currentTime = time();

        // Рекурсивне сканування директорії кешу
        try {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($this->cacheDir, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::SELF_FIRST
            );

            foreach ($iterator as $file) {
                if (!$file->isFile() || !$file->isReadable()) {
                    continue;
                }

                $filename = $file->getFilename();

                // Пропускаємо системні файли
                if (\in_array($filename, $systemFiles)) {
                    continue;
                }

                // Перевіряємо тільки файли кешу
                $extension = '.cache';
                if (\pathinfo($filename, PATHINFO_EXTENSION) !== \ltrim($extension, '.')) {
                    continue;
                }

                $filePath = $file->getPathname();
                $data = @\file_get_contents($filePath);

                if ($data === false) {
                    continue;
                }

                try {
                    $cached = \unserialize($data, ['allowed_classes' => false]);

                    if (!\is_array($cached) || !isset($cached['expires'])) {
                        @\unlink($filePath);
                        $cleaned++;
                        continue;
                    }

                    if ($cached['expires'] < $currentTime) {
                        @\unlink($filePath);
                        $cleaned++;
                    }
                } catch (Exception $e) {
                    // Видаляємо пошкоджений файл
                    self::logWarning('Cache::cleanup: Corrupted cache file detected and removed', [
                        'file' => $filePath,
                        'error' => $e->getMessage(),
                    ]);
                    @unlink($filePath);
                    $cleaned++;
                }
            }
        } catch (Exception $e) {
            self::logWarning('Cache::cleanup: Recursive scan failed, using fallback method', [
                'error' => $e->getMessage(),
                'exception' => $e,
            ]);
            // Якщо рекурсивне сканування не вдалося, використовуємо старий метод
            $extension = '.cache';
            $pattern = $this->cacheDir . '*' . $extension;
            $files = \glob($pattern);

            if ($files === false) {
                self::logError('Cache::cleanup: Failed to glob cache files', ['pattern' => $pattern]);
                return 0;
            }

            foreach ($files as $file) {
                if (!is_file($file) || !is_readable($file)) {
                    continue;
                }

                $filename = \basename($file);
                if (\in_array($filename, $systemFiles)) {
                    continue;
                }

                $data = @\file_get_contents($file);
                if ($data === false) {
                    continue;
                }

                try {
                    $cached = \unserialize($data, ['allowed_classes' => false]);

                    if (!\is_array($cached) || !isset($cached['expires'])) {
                        @\unlink($file);
                        $cleaned++;
                        continue;
                    }

                    if ($cached['expires'] < $currentTime) {
                        @\unlink($file);
                        $cleaned++;
                    }
                } catch (Exception $e) {
                    // Видаляємо пошкоджений файл
                    @unlink($file);
                    $cleaned++;
                }
            }
        }

        if ($cleaned > 0) {
            self::logInfo('Cache::cleanup: Cleanup completed', ['cleaned_files' => $cleaned]);
        } else {
            self::logDebug('Cache::cleanup: No expired files found');
        }

        return $cleaned;
    }

    /**
     * Отримати та видалити значення (реалізація інтерфейсу)
     *
     * @param string $key Ключ кешу
     * @param mixed $default Значення за замовчуванням
     * @return mixed
     */
    public function pull(string $key, mixed $default = null): mixed
    {
        $value = $this->get($key, $default);
        if ($value !== $default || $this->has($key)) {
            $this->delete($key);
        }
        return $value;
    }

    /**
     * Збільшити значення (реалізація інтерфейсу)
     *
     * @param string $key Ключ кешу
     * @param int $value Значення для збільшення
     * @return int Нове значення
     */
    public function increment(string $key, int $value = 1): int
    {
        $current = $this->get($key, 0);
        if (!is_numeric($current)) {
            $current = 0;
        }
        $newValue = (int)$current + $value;
        $this->set($key, $newValue);
        return $newValue;
    }

    /**
     * Зменшити значення (реалізація інтерфейсу)
     *
     * @param string $key Ключ кешу
     * @param int $value Значення для зменшення
     * @return int Нове значення
     */
    public function decrement(string $key, int $value = 1): int
    {
        $current = $this->get($key, 0);
        if (!is_numeric($current)) {
            $current = 0;
        }
        $newValue = (int)$current - $value;
        $this->set($key, $newValue);
        return $newValue;
    }

    /**
     * Отримання статистики кешу
     *
     * @return array<string, int|string>
     */
    public function getStats(): array
    {
        $extension = '.cache';
        $pattern = $this->cacheDir . '*' . $extension;
        $files = \glob($pattern);

        if ($files === false) {
            return [
                'total_files' => 0,
                'valid_files' => 0,
                'expired_files' => 0,
                'total_size' => 0,
                'memory_cache_size' => \count($this->memoryCache),
            ];
        }

        $totalSize = 0;
        $expired = 0;
        $valid = 0;
        $currentTime = \time();

        foreach ($files as $file) {
            if (!\is_file($file)) {
                continue;
            }

            $fileSize = @\filesize($file);
            if ($fileSize !== false) {
                $totalSize += $fileSize;
            }

            $data = @\file_get_contents($file);
            if ($data === false) {
                continue;
            }

            try {
                $cached = \unserialize($data, ['allowed_classes' => false]);

                if (!\is_array($cached) || !isset($cached['expires'])) {
                    $expired++;

                    continue;
                }

                // Перевіряємо термін дії (0 = без обмеження)
                if ($cached['expires'] !== 0 && $cached['expires'] < $currentTime) {
                    $expired++;
                } else {
                    $valid++;
                }
            } catch (Exception $e) {
                $expired++;
            }
        }

        return [
            'total_files' => \count($files),
            'valid_files' => $valid,
            'expired_files' => $expired,
            'total_size' => $totalSize,
            'memory_cache_size' => \count($this->memoryCache),
        ];
    }

    /**
     * Отримання імені файлу для ключа
     *
     * @param string $key Ключ кешу
     * @return string
     */
    private function getFilename(string $key): string
    {
        $hash = \md5($key);
        $extension = '.cache';

        return $this->cacheDir . $hash . $extension;
    }

    /**
     * Тегований кеш
     *
     * @param array|string $tags Теги
     * @return TaggedCache
     */
    public function tags($tags): TaggedCache
    {
        return new TaggedCache($this, (array)$tags);
    }

    // Запобігання клонуванню та десеріалізації
    private function __clone()
    {
    }

    /**
     * @return void
     * @throws Exception
     */
    public function __wakeup(): void
    {
        throw new Exception('Неможливо десеріалізувати singleton');
    }

    /**
     * Безпечне встановлення прав доступу на файл
     * Придушує всі попередження, щоб уникнути логування через Logger
     *
     * @param string $path Шлях до файлу
     * @param int $permissions Права доступу
     * @return void
     */
    private function setPermissions(string $path, int $permissions): void
    {
        // На Windows/WSL chmod може не працювати, тому перевіряємо ОС
        // Якщо це Windows, просто не викликаємо chmod
        if (\str_starts_with(\strtoupper(\PHP_OS), 'WIN')) {
            return;
        }

        // Зберігаємо поточний обробник помилок
        $oldErrorHandler = \set_error_handler(function ($errno, $errstr, $errfile, $errline) {
            // Ігноруємо тільки помилки chmod
            if ($errno === E_WARNING && \str_contains($errstr, 'chmod()')) {
                return true; // Ігноруємо помилку
            }

            // Для інших помилок повертаємо false, щоб викликати стандартний обробник
            return false;
        }, E_WARNING);

        // Зберігаємо поточний рівень повідомлень про помилки
        $originalErrorLevel = \error_reporting(0);

        // Тиха спроба встановити права доступу
        @\chmod($path, $permissions);

        // Відновлюємо рівень повідомлень про помилки
        \error_reporting($originalErrorLevel);

        // Відновлюємо старий обробник помилок
        \restore_error_handler();
        if ($oldErrorHandler !== null) {
            \set_error_handler($oldErrorHandler);
        }
    }
}

/**
 * Тегований кеш
 */
class TaggedCache
{
    private Cache $cache;
    private array $tags;

    /**
     * Конструктор
     *
     * @param Cache $cache Екземпляр кешу
     * @param array<string> $tags Масив тегів
     */
    public function __construct(Cache $cache, array $tags)
    {
        $this->cache = $cache;
        $this->tags = \array_filter($tags, function ($tag) {
            return \is_string($tag) && !empty($tag);
        });
    }

    /**
     * Отримання даних з кешу
     *
     * @param string $key Ключ
     * @param mixed $default Значення за замовчуванням
     * @return mixed
     */
    public function get(string $key, $default = null)
    {
        return $this->cache->get($this->taggedKey($key), $default);
    }

    /**
     * Збереження даних у кеш
     *
     * @param string $key Ключ
     * @param mixed $data Дані
     * @param int|null $ttl Час життя
     * @return bool
     */
    public function set(string $key, mixed $data, ?int $ttl = null): bool
    {
        $result = $this->cache->set($this->taggedKey($key), $data, $ttl);

        // Зберігаємо інформацію про теги
        foreach ($this->tags as $tag) {
            $tagKey = 'tag:' . $tag;
            $taggedKeys = $this->cache->get($tagKey, []);

            if (!\is_array($taggedKeys)) {
                $taggedKeys = [];
            }

            $taggedKeys[] = $this->taggedKey($key);
            $taggedKeys = \array_unique($taggedKeys);
            $this->cache->set($tagKey, $taggedKeys, 86400); // 24 години
        }

        return $result;
    }

    /**
     * Очищення всіх даних з вказаними тегами
     *
     * @return void
     */
    public function flush(): void
    {
        foreach ($this->tags as $tag) {
            $tagKey = 'tag:' . $tag;
            $taggedKeys = $this->cache->get($tagKey, []);

            if (\is_array($taggedKeys)) {
                foreach ($taggedKeys as $key) {
                    $this->cache->delete($key);
                }
            }

            $this->cache->delete($tagKey);
        }
    }

    /**
     * Генерація тегованого ключа
     *
     * @param string $key Ключ
     * @return string
     */
    private function taggedKey(string $key): string
    {
        $tagsStr = \implode(':', \array_map('md5', $this->tags));

        return 'tagged:' . $tagsStr . ':' . $key;
    }
}
