<?php

declare(strict_types=1);

namespace App\Core;

/**
 * CacheService
 *
 * Simple, dependency-free file-based cache (no Redis/Memcached
 * available in this environment, and none needed at this scale — a
 * single-server CRM for two business lines). Every cache file is one
 * JSON blob under storage/cache/, keyed by a sanitized filename, with
 * the expiry timestamp stored alongside the value so a stale read
 * never has to `stat()` the file separately.
 *
 * Used for genuinely expensive, frequently-repeated reads that don't
 * need to be real-time — dashboard summary cards, report aggregates —
 * NOT for anything a user just wrote (a lead they just created must
 * appear immediately; `remember()` is opt-in per call site, never a
 * blanket cache-everything layer).
 */
final class CacheService
{
    private static ?string $cacheDir = null;

    private static function dir(): string
    {
        if (self::$cacheDir === null) {
            self::$cacheDir = dirname(__DIR__, 2) . '/storage/cache';
            if (!is_dir(self::$cacheDir)) {
                @mkdir(self::$cacheDir, 0775, true);
            }
        }

        return self::$cacheDir;
    }

    /** Returns the cached value if present and unexpired, otherwise computes it via $callback, stores it, and returns it. */
    public static function remember(string $key, int $ttlSeconds, callable $callback): mixed
    {
        $cached = self::get($key);
        if ($cached !== null) {
            return $cached;
        }

        $value = $callback();
        self::set($key, $value, $ttlSeconds);

        return $value;
    }

    public static function get(string $key): mixed
    {
        $path = self::pathFor($key);
        if (!is_file($path)) {
            return null;
        }

        $raw = file_get_contents($path);
        if ($raw === false) {
            return null;
        }

        $decoded = json_decode($raw, true);
        if (!is_array($decoded) || !isset($decoded['expires_at'], $decoded['value'])) {
            return null;
        }

        if ($decoded['expires_at'] < time()) {
            @unlink($path);

            return null;
        }

        return $decoded['value'];
    }

    public static function set(string $key, mixed $value, int $ttlSeconds): void
    {
        $payload = json_encode(['expires_at' => time() + $ttlSeconds, 'value' => $value]);
        if ($payload === false) {
            return;
        }

        file_put_contents(self::pathFor($key), $payload, LOCK_EX);
    }

    public static function forget(string $key): void
    {
        @unlink(self::pathFor($key));
    }

    /** Clears every cached entry whose key starts with $prefix — e.g. forgetPrefix('dashboard:') after any write that could change dashboard numbers. */
    public static function forgetPrefix(string $prefix): void
    {
        $safePrefix = self::sanitize($prefix);
        foreach (glob(self::dir() . '/' . $safePrefix . '*.json') ?: [] as $file) {
            @unlink($file);
        }
    }

    private static function pathFor(string $key): string
    {
        return self::dir() . '/' . self::sanitize($key) . '.json';
    }

    private static function sanitize(string $key): string
    {
        return preg_replace('/[^A-Za-z0-9_\-:]/', '_', $key) ?? md5($key);
    }
}
