<?php

declare(strict_types=1);

namespace App\Framework\Cache\Driver;

use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Contracts\Scannable;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\Storage;

final readonly class FileCache implements CacheDriver, Scannable
{
    private const string CACHE_PATH = '/var/www/html/storage/cache';

    public function __construct(
        private Storage $fileSystem = new FileStorage(),
    ) {
        $this->fileSystem->createDirectory(self::CACHE_PATH);
    }

    private function getFileName(CacheKey $key, ?int $expiresAt): string
    {
        // Schütze vor Pfad/komischen Zeichen und Hash den Key
        $keyString = (string)$key;
        $safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $keyString);

        $hash = md5($safeKey);

        return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash .'_'. ($expiresAt ?? 0) . '.cache.php';
    }

    private function getFilesForKey(CacheKey $key): array
    {
        $keyString = (string)$key;
        // FIXED: Use same sanitization as getFileName() for consistent hashing
        $safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $keyString);
        $hash = md5($safeKey);

        $pattern = self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '*.cache.php';

        return glob($pattern) ?: [];
    }

    private function getLockFileName(CacheKey $key): string
    {
        $keyString = (string)$key;
        // FIXED: Use same sanitization as getFileName() for consistent hashing
        $safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $keyString);
        $hash = md5($safeKey);

        return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '.lock';
    }

    private function withKeyLock(CacheKey $key, callable $callback): mixed
    {
        $lockFile = fopen($this->getLockFileName($key), 'c');
        if ($lockFile === false) {
            // Optional: Fehler werfen oder Logging
            return $callback(null);
        }

        try {
            // Exklusiven Lock setzen, Wartezeit begrenzen wenn gewünscht
            if (flock($lockFile, LOCK_EX)) {
                return $callback($lockFile);
            }

            // Lock konnte nicht gesetzt werden
            return $callback(null);
        } finally {
            flock($lockFile, LOCK_UN);
            fclose($lockFile);
        }
    }

    private function getSingleKey(CacheKey $key): CacheItem
    {
        $bestFile = null;
        $bestExpires = null;

        foreach ($this->getFilesForKey($key) as $file) {
            if (! preg_match('/_(\d+)\.cache\.php$/', $file, $m)) {
                continue;
            }
            $expiresAt = (int)$m[1];
            if ($expiresAt > 0 && $expiresAt < time()) {
                $this->fileSystem->delete($file);

                continue;
            }
            if ($bestFile === null || $expiresAt > $bestExpires) {
                $bestFile = $file;
                $bestExpires = $expiresAt;
            }
        }

        if ($bestFile === null) {
            return CacheItem::miss($key);
        }

        try {
            $content = $this->fileSystem->get($bestFile);
        } catch (\App\Framework\Filesystem\Exceptions\FileNotFoundException $e) {
            // File was deleted between finding and reading (race condition)
            return CacheItem::miss($key);
        } catch (\App\Framework\Filesystem\Exceptions\FilePermissionException $e) {
            // File permissions changed or file being written (race condition)
            return CacheItem::miss($key);
        }

        if ($content === null || $content === '') {
            $this->fileSystem->delete($bestFile);

            return CacheItem::miss($key);
        }

        return CacheItem::hit($key, $content);
    }

    public function get(CacheKey ...$keys): CacheResult
    {
        if (empty($keys)) {
            return CacheResult::empty();
        }

        $items = [];
        foreach ($keys as $key) {
            $items[] = $this->getSingleKey($key);
        }

        return CacheResult::fromItems(...$items);
    }

    public function set(CacheItem ...$items): bool
    {
        if (empty($items)) {
            return true;
        }

        $success = true;
        foreach ($items as $item) {
            $result = $this->withKeyLock($item->key, function () use ($item) {
                $ttlSeconds = $item->ttl !== null ? $item->ttl->toCacheSeconds() : null;
                $expiresAt = $ttlSeconds ? (time() + $ttlSeconds) : null;

                foreach ($this->getFilesForKey($item->key) as $file) {
                    $this->fileSystem->delete($file);
                }

                $file = $this->getFileName($item->key, $expiresAt);

                // Store value directly as string (no serialization)
                $this->fileSystem->put($file, $item->value);

                return true;
            });

            $success = $success && $result;
        }

        return $success;
    }

    public function has(CacheKey ...$keys): array
    {
        if (empty($keys)) {
            return [];
        }

        $results = [];
        foreach ($keys as $key) {
            $keyString = (string)$key;
            $item = $this->getSingleKey($key);
            $results[$keyString] = $item->isHit;
        }

        return $results;
    }

    public function forget(CacheKey ...$keys): bool
    {
        if (empty($keys)) {
            return true;
        }

        $success = true;
        foreach ($keys as $key) {
            $result = $this->withKeyLock($key, function () use ($key) {
                foreach ($this->getFilesForKey($key) as $file) {
                    $this->fileSystem->delete($file);
                }

                return true;
            });

            $success = $success && $result;
        }

        return $success;
    }

    public function clear(): bool
    {
        $files = glob(self::CACHE_PATH . '/*.cache.php') ?: [];
        foreach ($files as $file) {
            $this->fileSystem->delete($file);
        }

        return true;
    }

    // === Scannable Interface Implementation ===

    public function scan(string $pattern, int $limit = 1000): array
    {
        $regex = $this->patternToRegex($pattern);
        $matches = [];
        $count = 0;

        $files = glob(self::CACHE_PATH . '/*.cache.php') ?: [];

        foreach ($files as $file) {
            if ($limit > 0 && $count >= $limit) {
                break;
            }

            $key = $this->fileToKey($file);
            if (preg_match($regex, $key)) {
                $matches[] = $key;
                $count++;
            }
        }

        return $matches;
    }

    public function scanPrefix(string $prefix, int $limit = 1000): array
    {
        $matches = [];
        $count = 0;

        $files = glob(self::CACHE_PATH . '/*.cache.php') ?: [];

        foreach ($files as $file) {
            if ($limit > 0 && $count >= $limit) {
                break;
            }

            $key = $this->fileToKey($file);
            if (str_starts_with($key, $prefix)) {
                $matches[] = $key;
                $count++;
            }
        }

        return $matches;
    }

    public function getAllKeys(int $limit = 1000): array
    {
        $keys = [];
        $count = 0;

        $files = glob(self::CACHE_PATH . '/*.cache.php') ?: [];

        foreach ($files as $file) {
            if ($limit > 0 && $count >= $limit) {
                break;
            }

            $keys[] = $this->fileToKey($file);
            $count++;
        }

        return $keys;
    }

    public function getScanPerformance(): array
    {
        return [
            'efficient' => false,
            'max_recommended_keys' => 1000,
            'estimated_time_per_1000_keys' => 0.1, // 100ms per 1000 keys
        ];
    }

    /**
     * Convert filename back to cache key
     */
    private function fileToKey(string $filepath): string
    {
        $filename = basename($filepath, '.cache.php');
        // Remove hash prefix if present
        if (strpos($filename, '_') !== false) {
            $parts = explode('_', $filename, 2);
            if (count($parts) === 2) {
                return $parts[1];
            }
        }

        return $filename;
    }

    /**
     * Convert wildcard pattern to regex
     */
    private function patternToRegex(string $pattern): string
    {
        // Escape special regex characters except * and **
        $escaped = preg_quote($pattern, '/');

        // Replace escaped wildcards back
        $escaped = str_replace('\\*\\*', '__DEEP_WILDCARD__', $escaped);
        $escaped = str_replace('\\*', '__WILDCARD__', $escaped);

        // Convert to regex
        $regex = str_replace('__DEEP_WILDCARD__', '.*', $escaped);
        $regex = str_replace('__WILDCARD__', '[^:]*', $regex);

        return '/^' . $regex . '$/';
    }
}
