<?php
namespace App\Services;

use App\Utils\Logger;

class KpiCacheService
{
    private string $sqlitePath;
    private string $fileCacheDir;
    private int $defaultTtl;
    private bool $enabled;
    private bool $sqliteAvailable = false;

    public function __construct()
    {
        $this->enabled = (getenv('KPI_CACHE_ENABLED') !== '0');
        $this->defaultTtl = max(10, (int)(getenv('KPI_CACHE_TTL_SECONDS') ?: 120));
        $cacheDir = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache';
        if (!is_dir($cacheDir)) {
            @mkdir($cacheDir, 0775, true);
        }
        $this->fileCacheDir = $cacheDir . DIRECTORY_SEPARATOR . 'files';
        if (!is_dir($this->fileCacheDir)) {
            @mkdir($this->fileCacheDir, 0775, true);
        }
        $this->sqlitePath = $cacheDir . DIRECTORY_SEPARATOR . 'kpi_cache.sqlite';
        if ($this->enabled) {
            $this->ensureSqliteSchema();
        }
    }

    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    public function get(string $surface, array $params): ?array
    {
        if (!$this->enabled) {
            return null;
        }

        $key = $this->buildKey($surface, $params);

        if (function_exists('apcu_fetch')) {
            $ok = false;
            $value = apcu_fetch($key, $ok);
            if ($ok && is_array($value)) {
                return $value;
            }
        }

        if ($file = $this->readFileCache($key)) {
            if (function_exists('apcu_store')) {
                @apcu_store($key, $file, $this->defaultTtl);
            }
            return $file;
        }

        try {
            $pdo = $this->sqlite();
            $stmt = $pdo->prepare("SELECT payload_json FROM cache_entries WHERE cache_key = ? AND expires_at_epoch > ? LIMIT 1");
            $stmt->execute([$key, time()]);
            $json = $stmt->fetchColumn();
            if ($json === false || $json === null) {
                return null;
            }
            $decoded = json_decode((string)$json, true);
            if (!is_array($decoded)) {
                return null;
            }
            if (function_exists('apcu_store')) {
                @apcu_store($key, $decoded, $this->defaultTtl);
            }
            return $decoded;
        } catch (\Throwable $e) {
            Logger::logWarning('KpiCacheService.get failed', ['error' => $e->getMessage()]);
            return null;
        }
    }

    public function set(string $surface, array $params, array $payload, ?int $ttl = null): void
    {
        if (!$this->enabled) {
            return;
        }
        $ttl = max(10, (int)($ttl ?? $this->defaultTtl));
        $key = $this->buildKey($surface, $params);
        $expiresAt = time() + $ttl;

        if (function_exists('apcu_store')) {
            @apcu_store($key, $payload, $ttl);
        }

        $this->writeFileCache($key, $payload, $expiresAt, $surface);

        try {
            $pdo = $this->sqlite();
            $stmt = $pdo->prepare("
                INSERT INTO cache_entries (cache_key, surface, scope_hash, payload_json, created_at_epoch, expires_at_epoch)
                VALUES (?, ?, ?, ?, ?, ?)
                ON CONFLICT(cache_key) DO UPDATE SET
                    payload_json = excluded.payload_json,
                    created_at_epoch = excluded.created_at_epoch,
                    expires_at_epoch = excluded.expires_at_epoch,
                    surface = excluded.surface,
                    scope_hash = excluded.scope_hash
            ");
            $stmt->execute([
                $key,
                $surface,
                $this->scopeHash($params),
                json_encode($payload, JSON_UNESCAPED_UNICODE),
                time(),
                $expiresAt
            ]);
            $this->maybeGc();
        } catch (\Throwable $e) {
            Logger::logWarning('KpiCacheService.set failed', ['error' => $e->getMessage()]);
        }
    }

    public function gc(bool $force = false): array
    {
        if (!$this->enabled) {
            return ['deleted' => 0, 'before' => 0, 'after' => 0];
        }
        try {
            $fileDeleted = $this->gcFiles($force);
            $pdo = $this->sqlite();
            if (!$force && random_int(1, 100) > 5) {
                return ['deleted' => 0, 'before' => 0, 'after' => 0, 'file_deleted' => $fileDeleted];
            }
            $before = (int)$pdo->query("SELECT COUNT(*) FROM cache_entries")->fetchColumn();
            $stmt = $pdo->prepare("DELETE FROM cache_entries WHERE expires_at_epoch <= ?");
            $stmt->execute([time()]);
            $deleted = (int)$stmt->rowCount();
            $after = (int)$pdo->query("SELECT COUNT(*) FROM cache_entries")->fetchColumn();
            return compact('deleted', 'before', 'after', 'fileDeleted');
        } catch (\Throwable $e) {
            // SQLite may be unavailable; file cache GC can still work.
            return ['deleted' => 0, 'before' => 0, 'after' => 0, 'fileDeleted' => $this->gcFiles($force)];
        }
    }

    private function maybeGc(): void
    {
        try {
            $this->gc(false);
        } catch (\Throwable $e) {
        }
    }

    private function buildKey(string $surface, array $params): string
    {
        return 'kpi:' . $surface . ':' . hash('sha256', $this->canonicalJson($this->normalizeParamsForKey($params)));
    }

    private function scopeHash(array $params): string
    {
        $subset = [
            'date_from' => $params['date_from'] ?? null,
            'date_to' => $params['date_to'] ?? null,
            'owner_didar_id' => $params['owner_didar_id'] ?? null,
            'owner_didar_ids_csv' => $params['owner_didar_ids_csv'] ?? null,
            'user_role' => $_SESSION['role'] ?? null,
            'session_didar_id' => $_SESSION['didar_id'] ?? null,
        ];
        return hash('sha1', $this->canonicalJson($subset));
    }

    private function normalizeParamsForKey(array $params): array
    {
        $copy = $params;
        unset(
            $copy['_verbose'],
            $copy['_json'],
            $copy['_trace_limit'],
            $copy['_only_card'],
            $copy['format']
        );
        $copy['__session_role'] = $_SESSION['role'] ?? '';
        $copy['__session_didar_id'] = $_SESSION['didar_id'] ?? '';
        $this->sortRecursive($copy);
        return $copy;
    }

    private function canonicalJson(array $data): string
    {
        $this->sortRecursive($data);
        return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private function sortRecursive(array &$arr): void
    {
        foreach ($arr as &$v) {
            if (is_array($v)) {
                $this->sortRecursive($v);
            }
        }
        unset($v);
        if ($this->isAssoc($arr)) {
            ksort($arr);
        } else {
            usort($arr, function ($a, $b) {
                return strcmp((string)json_encode($a), (string)json_encode($b));
            });
        }
    }

    private function isAssoc(array $arr): bool
    {
        return array_keys($arr) !== range(0, count($arr) - 1);
    }

    private function sqlite(): \PDO
    {
        if (!$this->sqliteAvailable) {
            throw new \RuntimeException('sqlite cache backend unavailable');
        }
        $pdo = new \PDO('sqlite:' . $this->sqlitePath);
        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
        return $pdo;
    }

    private function ensureSqliteSchema(): void
    {
        try {
            $pdo = new \PDO('sqlite:' . $this->sqlitePath);
            $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
            $pdo->exec("
                CREATE TABLE IF NOT EXISTS cache_entries (
                    cache_key TEXT PRIMARY KEY,
                    surface TEXT NOT NULL,
                    scope_hash TEXT,
                    payload_json TEXT NOT NULL,
                    created_at_epoch INTEGER NOT NULL,
                    expires_at_epoch INTEGER NOT NULL
                );
            ");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_cache_entries_expires ON cache_entries (expires_at_epoch)");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_cache_entries_surface ON cache_entries (surface)");
            $this->sqliteAvailable = true;
        } catch (\Throwable $e) {
            Logger::logWarning('KpiCacheService.ensureSqliteSchema failed', ['error' => $e->getMessage()]);
            $this->sqliteAvailable = false;
        }
    }

    private function readFileCache(string $key): ?array
    {
        $path = $this->filePathForKey($key);
        if (!is_file($path)) {
            return null;
        }
        $raw = @file_get_contents($path);
        if ($raw === false || $raw === '') {
            return null;
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return null;
        }
        if ((int)($decoded['expires_at_epoch'] ?? 0) <= time()) {
            @unlink($path);
            return null;
        }
        $payload = $decoded['payload'] ?? null;
        return is_array($payload) ? $payload : null;
    }

    private function writeFileCache(string $key, array $payload, int $expiresAt, string $surface): void
    {
        $path = $this->filePathForKey($key);
        $dir = dirname($path);
        if (!is_dir($dir)) {
            @mkdir($dir, 0775, true);
        }
        $json = json_encode([
            'surface' => $surface,
            'created_at_epoch' => time(),
            'expires_at_epoch' => $expiresAt,
            'payload' => $payload
        ], JSON_UNESCAPED_UNICODE);
        @file_put_contents($path, $json, LOCK_EX);
    }

    private function filePathForKey(string $key): string
    {
        $hash = hash('sha256', $key);
        $d1 = substr($hash, 0, 1);
        $d2 = substr($hash, 1, 1);
        return $this->fileCacheDir . DIRECTORY_SEPARATOR . $d1 . DIRECTORY_SEPARATOR . $d2 . DIRECTORY_SEPARATOR . $hash . '.json';
    }

    private function gcFiles(bool $force): int
    {
        if (!$force && random_int(1, 100) > 5) {
            return 0;
        }
        if (!is_dir($this->fileCacheDir)) {
            return 0;
        }
        $deleted = 0;
        $it = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($this->fileCacheDir, \FilesystemIterator::SKIP_DOTS)
        );
        foreach ($it as $file) {
            if (!$file->isFile() || strtolower($file->getExtension()) !== 'json') {
                continue;
            }
            $raw = @file_get_contents($file->getPathname());
            $decoded = is_string($raw) ? json_decode($raw, true) : null;
            $exp = (int)($decoded['expires_at_epoch'] ?? 0);
            if ($exp > 0 && $exp <= time()) {
                if (@unlink($file->getPathname())) {
                    $deleted++;
                }
            }
        }
        return $deleted;
    }
}
