<?php
namespace App\Helpers;
use App\Paste;
use DB;

use Exception;
use Illuminate\Database\Eloquent\Collection;
use Redis;
use RedisException;

/**
 * The global application cache.
 */
class Cache {
    private static Redis $redis;
    private static string $prefix;

    /**
     * The number of seconds for caching a view entry status.
     */
    public const TTL_PASTE_VIEW = 60;

    /**
     * The number of seconds for caching pinned pastes.
     */
    public const TTL_PINNED_PASTES = 120;

    /**
     * The list of pastes on the homepage.
     */
    public const TTL_HOMEPAGE_PASTES_LIST = 30;

    /**
     * @var string|null The user's view key, composed of IP address and user agent hashed.
     */
    private static ?string $userViewKey = null;

    /**
     * Initializes the global application cache.
     * @throws RedisException
     */
    public static function init() {
        self::$redis = new Redis();
        self::$prefix = env('REDIS_PREFIX') ?? '';
        if (self::$prefix === '') {
            self::$prefix = 'DOXBIN_';
        }

        $sock = env('REDIS_UNIX_SOCKET') ?? '';
        if ($sock === '') {
            $host = env('REDIS_HOST') ?? '';
            $port = (int)env('REDIS_PORT');

            // Default values.
            if ($host === '')
                $host = '127.0.0.1';
            if ($port === 0)
                $port = 6379;

            if (!self::$redis->connect($host, $port))
                throw new RedisException("Failed to connect to server.");
        } else {
            if (!self::$redis->connect($sock))
                throw new RedisException("Failed to connect to server.");
        }

        // Authenticate if necessary.
        $user = env('REDIS_USER') ?? '';
        $pass = env('REDIS_PASSWORD') ?? '';

        if ($pass !== '') {
            if ($user === '') {
                self::$redis->auth($pass);
            } else {
                self::$redis->auth([$user, $pass]);
            }
        }
    }

    /**
     * Sets a cache entry.
     * If $ttlSeconds is more than 0, the cache entry will live for that number of seconds before being deleted.
     * @param string $key The cache key.
     * @param mixed $value The cache value.
     * @param int $ttlSeconds The number of seconds that the cache entry will live for, or less than 1 to never expire (defaults to 0).
     * @return void
     */
    public static function set(string $key, $value, int $ttlSeconds = 0) {
        $keyRes = self::$prefix . $key;

        self::$redis->set($keyRes, serialize($value));

        if ($ttlSeconds > 0)
            self::$redis->expire($keyRes, $ttlSeconds);
    }

    /**
     * Gets a cached entry.
     * If $ttlSeconds is more than 0, the cache entry will live for that number of seconds before being deleted.
     * @param string $key The cache key.
     * @param bool $delete Whether to delete the entry after retrieving it (defaults to false).
     * @template T The return type.
     * @return T|null The cached entry, or null if it did not exist.
     */
    public static function get(string $key, bool $delete = false) {
        $keyRes = self::$prefix . $key;

        if (self::$redis->exists($keyRes) < 1) {
            return null;
        } else {
            $val = unserialize(self::$redis->get($keyRes));

            if ($delete)
                self::delete($keyRes);

            return $val;
        }
    }

    /**
     * Deletes a key or keys (matching a pattern) and returns the number of values deleted.
     * @param string $key The key pattern to delete (supports asterisk wildcard).
     * @return int The number of values deleted.
     */
    public static function delete(string $key): int {
        return self::$redis->del(self::$prefix . $key);
    }

    /**
     * Returns whether the specified key exists.
     * @param string $key The key for which to check.
     * @return bool Whether the key exists.
     */
    public static function exists(string $key): bool {
        return self::$redis->exists(self::$prefix . $key) > 0;
    }

    /**
     * @return string Returns the user's paste view key, composed of IP address and user agent.
     */
    private static function getUserViewKey(): string {
        if (self::$userViewKey === null) {
            $ip = $_SERVER[env('REAL_IP_HEADER')];
            $ip = substr($ip, strlen($ip) - 6, strlen($ip));

            self::$userViewKey = md5($ip . $_SERVER['HTTP_USER_AGENT'] ?? '');
        }

        return self::$userViewKey;
    }

    /**
     * Returns whether the user has viewed the paste with the specified ID.
     * It first checks for a cached view entry, and then performs an SQL query if it did not find out.
     * Use markUserHasViewedPaste to set the cache entry for its TTL.
     * @param string $id The paste ID.
     * @return bool Whether the paste has been viewed by the user.
     */
    public static function hasUserViewedPaste(string $id): bool {
        $viewKey = self::getUserViewKey();
        $cacheKey = 'HAS_VIEWED_' . $viewKey . '_' . $id;

        $has = self::get($cacheKey);

        if ($has === null) {
            $has = DB::table('views')->where(['ua' => $viewKey, 'id' => $id])->exists();
            self::set($cacheKey, $has, self::TTL_PASTE_VIEW);
        }

        return $has;
    }

    /**
     * Marks a paste as viewed by the user.
     * @param string $id The paste ID.
     * @return void
     */
    public static function markUserHasViewedPaste(string $id): void {
        $viewKey = self::getUserViewKey();
        $cacheKey = 'HAS_VIEWED_' . $viewKey . '_' . $id;

        self::set($cacheKey, true, self::TTL_PASTE_VIEW);
    }

    /**
     * Returns pinned pastes list.
     * Caches the value for self::TTL_PINNED_PASTES seconds.
     * @return Collection|null The pinned pastes list.
     */
    public static function getPinnedPastes(): ?Collection {
        $key = 'PINNED_PASTES';

        if (self::exists($key)) {
            $val = self::get($key);
        } else {
            $ids = DB::table('pinned_pastes')->pluck('id');
            $val = Paste::whereIn('pastes.id', $ids)->leftJoin('users', 'users.id', '=', 'pastes.created_by')->where('hidden', 0)->latest('created_at')->get(PASTES_LIST_COLUMNS_TO_SELECT);

            self::set($key, $val, self::TTL_PINNED_PASTES);
        }

        return $val;
    }

    /**
     * Clears the cached pinned pastes list.
     * Has no effect if there was no cached list.
     * @return void
     */
    public static function clearPinnedPastesCache(): void {
        self::delete('PINNED_PASTES');
    }
}

try {
    Cache::init();
} catch (Exception $e) {
    // logger destroys everything on php8
    // logger('ERROR: Failed to connect to caching server: ' . $e->getMessage());
    die('Failed to connect to caching server. Could not continue.');
}
