<?php
/**
 * @package     MefisAssetBridge
 * @subpackage  MefisAssetBridgeCore_Cache
 *
 * @copyright   Copyright (C) 2025 Goran Usljebrka. All rights reserved.
 * @license     Proprietary — No resale or incorporation into other projects without explicit written permission. see LICENSE.txt
 */

namespace MefisAssetBridgeCore\Cache;

/**
 * CoreCache — CMS-agnostic file-based cache for asset data and thumbnails.
 *
 * Cache directory is injected by the CMS layer (Joomla/WordPress) so this
 * class has zero CMS dependencies.
 *
 * Cache files are stored as encrypted binaries exactly as received from AWS S3.
 * Decryption always happens on read — the cache never stores plaintext.
 *
 * File layout:
 *   {cacheDir}/{userUuid}/{assetUuid}.data   — encrypted asset data binary
 *   {cacheDir}/{userUuid}/{assetUuid}.thumb  — encrypted thumbnail binary
 */
class CoreCache
{
    /** @var string Absolute path to the cache root directory */
    private string $cacheDir;

    /**
     * @param string $cacheDir Absolute path to the cache root directory.
     *                         Must be writable. Created automatically if absent.
     */
    public function __construct(string $cacheDir)
    {
        $this->cacheDir = rtrim($cacheDir, DIRECTORY_SEPARATOR);
    }

    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------

    /**
     * Read a cached file.
     *
     * Returns file contents or null when:
     *   - file does not exist
     *   - $timestamp supplied and filemtime() < $timestamp  (cache is older than asset's modifiedAt)
     *
     * @param string   $userUuid  User UUID (cache sub-directory)
     * @param string   $fileName  Cache file name  (e.g. "{assetUuid}.data")
     * @param int|null $timestamp Asset modifiedAt as Unix timestamp for staleness check
     *
     * @return string|null Raw file contents, or null on miss/stale
     */
    public function get(string $userUuid, string $fileName, ?int $timestamp = null): ?string
    {
        $filePath = $this->filePath($userUuid, $fileName);

        if (!file_exists($filePath)) {
            return null;
        }

        if ($timestamp !== null && filemtime($filePath) < $timestamp) {
            return null;
        }

        $contents = file_get_contents($filePath);
        return $contents !== false ? $contents : null;
    }

    /**
     * Write string content to cache.
     *
     * @param string $userUuid User UUID (cache sub-directory)
     * @param string $content  Binary or string content to store
     * @param string $fileName Cache file name
     *
     * @return bool True on success
     */
    public function set(string $userUuid, string $content, string $fileName): bool
    {
        try {
            $this->ensureDir($userUuid);
            return file_put_contents($this->filePath($userUuid, $fileName), $content) !== false;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Copy an existing file into the cache (used when source already exists as a temp file).
     *
     * @param string $userUuid       User UUID (cache sub-directory)
     * @param string $srcPath        Absolute path to source file
     * @param string $fileName       Cache file name
     *
     * @return bool True on success
     */
    public function setFromFile(string $userUuid, string $srcPath, string $fileName): bool
    {
        if (!file_exists($srcPath)) {
            return false;
        }
        try {
            $this->ensureDir($userUuid);
            return copy($srcPath, $this->filePath($userUuid, $fileName));
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Check whether a cached file is older than the given timestamp.
     *
     * Returns true (= stale) when file does not exist.
     *
     * @param string $userUuid  User UUID
     * @param string $fileName  Cache file name
     * @param int    $timestamp Unix timestamp to compare against
     *
     * @return bool
     */
    public function isOlderThan(string $userUuid, string $fileName, int $timestamp): bool
    {
        $filePath = $this->filePath($userUuid, $fileName);
        return !file_exists($filePath) || filemtime($filePath) < $timestamp;
    }

    /**
     * Delete a single cached file.
     *
     * @param string $userUuid User UUID
     * @param string $fileName Cache file name
     *
     * @return bool
     */
    public function delete(string $userUuid, string $fileName): bool
    {
        $filePath = $this->filePath($userUuid, $fileName);
        if (file_exists($filePath)) {
            return @unlink($filePath);
        }
        return true;
    }

    /**
     * Delete all cached files for one asset (both .data and .thumb).
     *
     * @param string $userUuid  User UUID
     * @param string $assetUuid Asset UUID
     *
     * @return void
     */
    public function deleteAsset(string $userUuid, string $assetUuid): void
    {
        $this->delete($userUuid, $assetUuid . '.data');
        $this->delete($userUuid, $assetUuid . '.thumb');
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private function filePath(string $userUuid, string $fileName): string
    {
        return $this->cacheDir . DIRECTORY_SEPARATOR . $userUuid . DIRECTORY_SEPARATOR . $fileName;
    }

    private function ensureDir(string $userUuid): void
    {
        $dir = $this->cacheDir . DIRECTORY_SEPARATOR . $userUuid;

        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }

        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
            @file_put_contents($dir . DIRECTORY_SEPARATOR . 'index.php', '<?php http_response_code(403); exit;' . PHP_EOL);
        }
    }
}
