<?php

declare(strict_types=1);

namespace App\Http\Controllers\Helpers;

use Hyperf\Coroutine\Coroutine;
use Hypervel\Support\Facades\Cache;
use Hyperf\Database\Model\Builder as ModelBuilder;
use Hyperf\Database\Query\Builder as QueryBuilder;
use Hypervel\Database\Eloquent\Model;

class CacheHelper
{
    /**
     * Get data from cache.
     * 
     * @param mixed $query Can be SQL string or Builder object
     * @param mixed $params Parameters to hash (array, object, or model)
     * @return mixed|null
     */
    public static function get_cache($query, $params = [])
    {
        try {
            $key = self::generateKey($query, $params);
            return Cache::get($key);
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Set data in cache using a background coroutine (fire-and-forget).
     * 
     * @param mixed $query Can be SQL string or Builder object
     * @param mixed $params Parameters to hash
     * @param mixed $data Data to cache
     * @param int $ttl Time to live in seconds (default: 1 day)
     */
    public static function set_cache($query, $data, $params = [], int $ttl = 86400)
    {
        $key = self::generateKey($query, $params);

        // Fire-and-forget coroutine: we don't wait for completion
        Coroutine::create(function () use ($key, $data, $ttl) {
            try {
                Cache::put($key, $data, $ttl);
            } catch (\Throwable $e) {
                // Silently fail as cache operations are non-critical
            }
        });
    }

    /**
     * Erase a specific cache entry.
     * 
     * @param mixed $query Can be SQL string or Builder object
     * @param mixed $params Parameters to hash
     */
    public static function erase_cache($query, $params = [])
    {
        try {
            $key = self::generateKey($query, $params);
            Cache::forget($key);
        } catch (\Throwable $e) {
            // Silently fail
        }
    }

    /**
     * Generate the cache key using the specific format:
     * querycache:[queryhash]++++[parameterhash]
     */
    public static function generateKey($query, $params): string
    {
        $sql = '';
        $parameterData = $params;

        // Automatically handle Builder objects
        if ($query instanceof ModelBuilder || $query instanceof QueryBuilder) {
            $sql = $query->toSql();
            // If explicit params were not provided or empty, use the builder's bindings
            if (empty($params)) {
                $parameterData = $query->getBindings();
            }
        } else {
            $sql = (string) $query;
        }

        $queryHash = md5($sql);
        $paramHash = md5(self::serializeParams($parameterData));

        return "querycache:{$queryHash}++++{$paramHash}";
    }

    /**
     * Deep serialization of parameters for hashing.
     * Supports arrays, objects, and models.
     */
    private static function serializeParams($params): string
    {
        if (is_array($params)) {
            $processed = [];
            foreach ($params as $key => $value) {
                $processed[$key] = self::serializeParams($value);
            }
            // Sort keys to ensure consistent hashing for the same associative array
            ksort($processed);
            return json_encode($processed);
        }

        if (is_object($params)) {
            // Handle models specifically
            if ($params instanceof Model) {
                return get_class($params) . ':' . ($params->hashkey ?? $params->id ?? 'new');
            }
            
            if (method_exists($params, 'toArray')) {
                return json_encode($params->toArray());
            }

            return get_class($params) . ':' . spl_object_hash($params);
        }

        return (string) $params;
    }
}
