<?php

namespace App\Framework\Cache\Driver;

use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\Storage;

final readonly class FileCache implements CacheDriver
{
    private const string CACHE_PATH = __DIR__ . '/../storage/cache';

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

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

        $hash = md5($safeKey);

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

    private function getFilesForKey(string $key): array
    {
        $hash = md5($key);

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

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

    private function getLockFileName(string $key): string
    {
        $hash = md5($key);
        return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '.lock';
    }

    private function withKeyLock(string $key, callable $callback)
    {
        $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);
        }
    }


    public function get(string $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);
        }

        $content = $this->fileSystem->get($bestFile);

        $data = @unserialize($content) ?: [];


        if (!isset($data['value'])) {
            $this->fileSystem->delete($bestFile);
            return CacheItem::miss($key);
        }

        return CacheItem::hit($key, $data['value']);

    }

    public function set(string $key, mixed $value, ?int $ttl = null): bool
    {
        return $this->withKeyLock($key, function () use ($key, $value, $ttl) {

            $expiresAt = $ttl ? (time() + $ttl) : null;

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

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

            $data = [
                'value' => $value,
                'expires_at' => $expiresAt,
            ];

            $this->fileSystem->put($file, serialize($data));

            return true;
        });
    }

    public function has(string $key): bool
    {
        $item = $this->get($key);
        return $item->isHit;
    }

    public function forget(string $key): bool
    {
        return $this->withKeyLock($key, function () use ($key) {

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

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

        return true;
    }
}
