<?php
/**
 * FluentC Cache Management Class.
 *
 * This file contains an improved caching system for FluentC
 *
 * @package  FluentCPlugin
 * @author   FluentC <wp@fluentc.io>
 */

namespace FluentC\Services;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Improved cache system with support for multiple backends and better performance
 *
 * @since 2.4
 */
class Cache {
    /**
     * Current site ID.
     *
     * @var int
     */
    protected int $blog_id;

    /**
     * The plugin version used for cache versioning.
     *
     * @var string
     */
    protected string $version;

    /**
     * Cache groups for better organization
     *
     * @var array
     */
    protected array $groups = [
        'languages',    // Language lists and settings
        'translations', // Translated content
        'settings',     // Plugin settings
        'urls',         // Site URLs and structure
        'temp'          // Temporary data that can be purged frequently
    ];

    /**
     * Whether to use object caching if available
     *
     * @var bool
     */
    protected bool $use_object_cache;

    /**
     * Constructor.
     *
     * @since 2.4
     */
    public function __construct() {
        $this->blog_id = get_current_blog_id();
        
        // Always use the version from the main plugin file
        // The constant should already be defined in fluentc_wordpress_plugin.php
        $this->version = FLUENTC_TRANSLATION_VERSION;
        
        // Determine if we should use object cache (when available)
        $this->use_object_cache = wp_using_ext_object_cache() && !defined('FLUENTC_DISABLE_OBJECT_CACHE');
        
        // Register cache flush on blog switch
        add_action('switch_blog', [$this, 'switch_blog']);
    }

    /**
     * Called when switching blog.
     *
     * @since 2.4
     *
     * @param  int $new_blog_id New blog ID.
     * @return void
     */
    public function switch_blog(int $new_blog_id): void {
        $this->blog_id = $new_blog_id;
    }

    /**
     * Generate a standardized cache key with versioning and namespacing
     *
     * @param string $key The base cache key
     * @param string $group The cache group (default: 'translations')
     * @return string The formatted cache key
     */
    protected function get_cache_key(string $key, string $group = 'translations'): string {
        // Sanitize group
        $group = in_array($group, $this->groups, true) ? $group : 'translations';
        
        // Key format: fluentc_{blog_id}_{version}_{group}_{key}
        return sprintf('fluentc_%d_%s_%s_%s', $this->blog_id, $this->version, $group, $key);
    }

    /**
     * Add a value to the cache with staggered expiration.
     *
     * @since 2.4
     *
     * @param string $key Cache key.
     * @param mixed $data The value to add to the cache.
     * @param string $group Cache group.
     * @param int $expiration Expiration time in seconds (default: 1 day).
     * @return bool True on success, false on failure.
     */
    public function set(string $key, $data, string $group = 'translations', int $expiration = DAY_IN_SECONDS): bool {
        // Add a random value (0-10% of expiration) to prevent cache stampedes
        
        $cache_key = $this->get_cache_key($key, $group);
        
        // Use object cache if available, otherwise fall back to transients
        if ($this->use_object_cache) {
            return wp_cache_set($cache_key, $data, 'fluentc', $expiration);
        } else {
            return set_transient($cache_key, $data, $expiration);
        }
    }

    /**
     * Get value from cache.
     *
     * @since 2.4
     *
     * @param string $key Cache key.
     * @param string $group Cache group.
     * @param mixed $default Default value to return if cache miss.
     * @return mixed The cached value or $default.
     */
    public function get(string $key, string $group = 'translations', $default = false) {
        $cache_key = $this->get_cache_key($key, $group);
        
        if ($this->use_object_cache) {
            $data = wp_cache_get($cache_key, 'fluentc');
            return false !== $data ? $data : $default;
        } else {
            $data = get_transient($cache_key);
            return false !== $data ? $data : $default;
        }
    }

    /**
     * Check if a key exists in the cache.
     *
     * @since 2.4
     *
     * @param string $key Cache key.
     * @param string $group Cache group.
     * @return bool True if the key exists, false otherwise.
     */
    public function has(string $key, string $group = 'translations'): bool {
        return false !== $this->get($key, $group);
    }

    /**
     * Delete a value from cache.
     *
     * @since 2.4
     *
     * @param string $key Cache key.
     * @param string $group Cache group.
     * @return bool True on success, false on failure.
     */
    public function delete(string $key, string $group = 'translations'): bool {
        $cache_key = $this->get_cache_key($key, $group);
        
        if ($this->use_object_cache) {
            return wp_cache_delete($cache_key, 'fluentc');
        } else {
            return delete_transient($cache_key);
        }
    }

    /**
     * Get multiple values from cache.
     *
     * @since 2.4
     *
     * @param array $keys Array of cache keys.
     * @param string $group Cache group.
     * @return array Associative array of cache key => cache value pairs.
     */
    public function get_multiple(array $keys, string $group = 'translations'): array {
        $results = [];
        
        if ($this->use_object_cache && function_exists('wp_cache_get_multiple')) {
            $cache_keys = [];
            foreach ($keys as $key) {
                $cache_keys[] = $this->get_cache_key($key, $group);
            }
            
            $values = wp_cache_get_multiple($cache_keys, 'fluentc');
            
            // Reindex with original keys
            foreach ($keys as $index => $key) {
                $cache_key = $this->get_cache_key($key, $group);
                $results[$key] = $values[$cache_key] ?? false;
            }
        } else {
            // Fall back to individual gets
            foreach ($keys as $key) {
                $results[$key] = $this->get($key, $group);
            }
        }
        
        return $results;
    }

    /**
     * Set multiple values in cache.
     *
     * @since 2.4
     *
     * @param array $data Associative array of key => value pairs to cache.
     * @param string $group Cache group.
     * @param int $expiration Time until expiration in seconds (default: 1 day).
     * @return bool True on success, false on failure.
     */
    public function set_multiple(array $data, string $group = 'translations', int $expiration = DAY_IN_SECONDS): bool {
        $success = true;
        
        foreach ($data as $key => $value) {
            $result = $this->set($key, $value, $group, $expiration);
            $success = $success && $result;
        }
        
        return $success;
    }

    /**
     * Clean all cache for a specific group or all groups
     *
     * @since 2.4
     *
     * @param string|null $group Optional group to clean. Null for all groups.
     * @return bool True on success, false on failure.
     */
    public function clean(?string $group = null): bool {
        if ($this->use_object_cache) {
            if ($group === null) {
                // Clean all groups
                wp_cache_flush_group('fluentc');
                return true;
            }
            
            // Verify group exists
            if (!in_array($group, $this->groups, true)) {
                return false;
            }
            
            // Use a deterministic pattern to clean all keys in a group
            $group_pattern = sprintf('fluentc_%d_%s_%s_', $this->blog_id, $this->version, $group);
            
            // This is a simple approximation - actual implementation would depend on the object cache backend
            // Some cache backends support group or pattern-based deletion
            wp_cache_flush_group('fluentc');
            return true;
        } else {
            // We have to use a database query for transients
            return $this->clean_transients($group);
        }
    }

    /**
     * Clean transients matching a specific pattern
     * 
     * @since 2.4
     * 
     * @param string|null $group Optional group to clean
     * @return bool True on success
     */
    protected function clean_transients(?string $group = null): bool {
        global $wpdb;
        
        // Build the SQL WHERE clause based on group
        if ($group !== null && in_array($group, $this->groups, true)) {
            $pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_' . $this->version . '_' . $group) . '%';
            $timeout_pattern = $wpdb->esc_like('_transient_timeout_fluentc_' . $this->blog_id . '_' . $this->version . '_' . $group) . '%';
        } else {
            $pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_') . '%';
            $timeout_pattern = $wpdb->esc_like('_transient_timeout_fluentc_' . $this->blog_id . '_') . '%';
        }
        
        // Use prepared statements for security
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
            $pattern,
            $timeout_pattern
        ));
        
        return $wpdb->last_error === '';
    }

    /**
     * Update a translation in cache
     *
     * @since 2.4
     *
     * @param string $translation_id The ID of the translation to update
     * @param string $new_translation The new translated text
     * @return array Result status and message
     */
    public function update_translation(string $translation_id, string $new_translation): array {
        // Get the cached translation
        $transient_data = $this->get($translation_id, 'translations');
        
        if (!$transient_data) {
            return [
                'success' => false,
                'message' => 'Translation not found in cache',
            ];
        }
        
        // Decode from JSON if it's a string
        if (is_string($transient_data)) {
            $data = json_decode($transient_data, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                return [
                    'success' => false,
                    'message' => 'Failed to parse cached translation data',
                ];
            }
        } else {
            $data = $transient_data;
        }
        
        // Update the translation
        if (isset($data['data']['translateSite']['body'][0]['translatedText'])) {
            // Remove any potential slashes added by WordPress
            $cleaned_translation = wp_unslash($new_translation);
            
            // Ensure the text is stored as plain text, not HTML entities
            $cleaned_translation = html_entity_decode($cleaned_translation, ENT_QUOTES | ENT_HTML5, 'UTF-8');
            
            $data['data']['translateSite']['body'][0]['translatedText'] = $cleaned_translation;
            
            // Use JSON_UNESCAPED_UNICODE and JSON_UNESCAPED_SLASHES for clean storage
            $new_transient_data = wp_json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            
            // Set the updated translation
            $this->set($translation_id, $new_transient_data, 'translations');
            
            return [
                'success' => true,
                'message' => 'Translation updated successfully',
            ];
        } elseif (isset($data['data']['translateSite']['body']['translatedText'])) {
            // Handle the alternate data structure
            $cleaned_translation = wp_unslash($new_translation);
            $cleaned_translation = html_entity_decode($cleaned_translation, ENT_QUOTES | ENT_HTML5, 'UTF-8');
            
            $data['data']['translateSite']['body']['translatedText'] = $cleaned_translation;
            
            $new_transient_data = wp_json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            $this->set($translation_id, $new_transient_data, 'translations');
            
            return [
                'success' => true,
                'message' => 'Translation updated successfully',
            ];
        }
        
        return [
            'success' => false,
            'message' => 'Translation data structure not recognized',
        ];
    }

    /**
     * Search for translations matching a term
     *
     * @since 2.4
     *
     * @param string $search_term The search term
     * @param int $offset Pagination offset
     * @param int $limit Items per page
     * @return array Matching translations
     */
    public function search_translations(string $search_term, int $offset = 0, int $limit = 20): array {
        // If we're using object cache, we need to retrieve and filter in PHP
        if ($this->use_object_cache) {
            return $this->search_translations_in_memory($search_term, $offset, $limit);
        }
        
        // Otherwise use database query for transients
        return $this->search_translations_in_db($search_term, $offset, $limit);
    }
    
    /**
     * Search for translations in database
     *
     * @since 2.4
     *
     * @param string $search_term The search term
     * @param int $offset Pagination offset
     * @param int $limit Items per page
     * @return array Matching translations
     */
    protected function search_translations_in_db(string $search_term, int $offset = 0, int $limit = 20): array {
        global $wpdb;
        
        $search_pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_' . $this->version . '_translations_') . '%';
        $like_term = '%' . $wpdb->esc_like($search_term) . '%';
        
        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT option_name, option_value 
                FROM {$wpdb->options} 
                WHERE option_name LIKE %s 
                AND option_value LIKE %s
                LIMIT %d, %d",
                $search_pattern,
                $like_term,
                $offset,
                $limit
            )
        );
        
        return $this->process_translation_results($results, $search_term);
    }
    
    /**
     * Search for translations in memory
     *
     * @since 2.4
     *
     * @param string $search_term The search term
     * @param int $offset Pagination offset
     * @param int $limit Items per page
     * @return array Matching translations
     */
    protected function search_translations_in_memory(string $search_term, int $offset = 0, int $limit = 20): array {
        // This is a simplified implementation - real-world would need pagination strategy
        $all_translations = [];
        $search_term_lower = mb_strtolower($search_term);
        
        // Get list of translation keys (potentially expensive)
        $translation_keys = $this->get_translation_keys();
        
        foreach ($translation_keys as $key) {
            $data = $this->get($key, 'translations');
            if (!$data) {
                continue;
            }
            
            // Parse the data
            if (is_string($data)) {
                $parsed = json_decode($data, true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    continue;
                }
            } else {
                $parsed = $data;
            }
            
            // Extract and check translation text
            $translation = $this->extract_translation_data($parsed);
            if (!$translation) {
                continue;
            }
            
            // Check if the translation matches the search term
            if (mb_stripos($translation['originalText'], $search_term_lower) !== false || 
                mb_stripos($translation['translatedText'], $search_term_lower) !== false) {
                $translation['id'] = $key;
                $all_translations[] = $translation;
                
                // Limit results for performance
                if (count($all_translations) >= ($offset + $limit)) {
                    break;
                }
            }
        }
        
        // Apply pagination
        return array_slice($all_translations, $offset, $limit);
    }
    
    /**
     * Get list of translation keys
     *
     * @since 2.4
     *
     * @return array List of translation keys
     */
    protected function get_translation_keys(): array {
        global $wpdb;
        
        $pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_' . $this->version . '_translations_') . '%';
        
        $keys = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
                $pattern
            )
        );
        
        // Strip the transient prefix
        return array_map(function($key) {
            return str_replace('_transient_', '', $key);
        }, $keys);
    }
    
    /**
     * Process translation database results into a standardized format
     *
     * @since 2.4
     *
     * @param array $results Database results
     * @param string $search_term Original search term for verification
     * @return array Standardized translations
     */
    protected function process_translation_results(array $results, string $search_term): array {
        $translations = [];
        $search_term_lower = mb_strtolower($search_term);
        
        foreach ($results as $result) {
            $transient_name = str_replace('_transient_', '', $result->option_name);
            $transient_name = preg_replace('/^fluentc_\d+_[^_]+_translations_/', '', $transient_name);
            
            $raw_data = $result->option_value;
            $data = json_decode($raw_data, true);
            
            if (json_last_error() !== JSON_ERROR_NONE) {
                continue;
            }
            
            $translation = $this->extract_translation_data($data);
            if (!$translation) {
                continue;
            }
            
            // Double-check search term matches (in case DB collation differs)
            if (mb_stripos($translation['originalText'], $search_term_lower) !== false || 
                mb_stripos($translation['translatedText'], $search_term_lower) !== false) {
                $translation['id'] = $transient_name;
                $translations[] = $translation;
            }
        }
        
        return $translations;
    }
    
    /**
     * Extract translation data from parsed JSON
     *
     * @since 2.4
     *
     * @param array $data Parsed JSON data
     * @return array|null Extracted translation data or null if not found
     */
    protected function extract_translation_data(array $data): ?array {
        if (isset($data['data']['translateSite']['body'][0])) {
            return [
                'sourceLanguage' => $data['data']['translateSite']['body'][0]['sourceLanguage'] ?? '',
                'targetLanguage' => $data['data']['translateSite']['body'][0]['targetLanguage'] ?? '',
                'originalText' => html_entity_decode(
                    $data['data']['translateSite']['body'][0]['originalText'] ?? '', 
                    ENT_QUOTES | ENT_HTML5, 
                    'UTF-8'
                ),
                'translatedText' => html_entity_decode(
                    $data['data']['translateSite']['body'][0]['translatedText'] ?? '', 
                    ENT_QUOTES | ENT_HTML5, 
                    'UTF-8'
                )
            ];
        } elseif (isset($data['data']['translateSite']['body']['originalText'])) {
            return [
                'sourceLanguage' => $data['data']['translateSite']['body']['sourceLanguage'] ?? '',
                'targetLanguage' => $data['data']['translateSite']['body']['targetLanguage'] ?? '',
                'originalText' => html_entity_decode(
                    $data['data']['translateSite']['body']['originalText'] ?? '', 
                    ENT_QUOTES | ENT_HTML5, 
                    'UTF-8'
                ),
                'translatedText' => html_entity_decode(
                    $data['data']['translateSite']['body']['translatedText'] ?? '', 
                    ENT_QUOTES | ENT_HTML5, 
                    'UTF-8'
                )
            ];
        }
        
        return null;
    }
    
    /**
     * Count total translations matching a search term
     *
     * @since 2.4
     *
     * @param string $search_term The search term
     * @return int Total count of matching translations
     */
    public function count_translation_matches(string $search_term): int {
        global $wpdb;
        
        $search_pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_' . $this->version . '_translations_') . '%';
        $like_term = '%' . $wpdb->esc_like($search_term) . '%';
        
        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) 
                FROM {$wpdb->options} 
                WHERE option_name LIKE %s 
                AND option_value LIKE %s",
                $search_pattern,
                $like_term
            )
        );
    }
    
    /**
     * Get all translations (paginated)
     *
     * @since 2.4
     *
     * @param int $offset Pagination offset
     * @param int $limit Items per page
     * @return array Translations
     */
    public function get_all_translations(int $offset = 0, int $limit = 20): array {
        global $wpdb;
        
        $pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_' . $this->version . '_translations_') . '%';
        
        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT option_name, option_value 
                FROM {$wpdb->options} 
                WHERE option_name LIKE %s
                ORDER BY option_id DESC
                LIMIT %d, %d",
                $pattern,
                $offset,
                $limit
            )
        );
        
        $translations = [];
        
        foreach ($results as $result) {
            $transient_name = str_replace('_transient_', '', $result->option_name);
            $transient_name = preg_replace('/^fluentc_\d+_[^_]+_translations_/', '', $transient_name);
            
            $raw_data = $result->option_value;
            $data = json_decode($raw_data, true);
            
            if (json_last_error() !== JSON_ERROR_NONE) {
                continue;
            }
            
            $translation = $this->extract_translation_data($data);
            if (!$translation) {
                continue;
            }
            
            $translation['id'] = $transient_name;
            $translations[] = $translation;
        }
        
        return $translations;
    }
    
    /**
     * Count all translations
     *
     * @since 2.4
     *
     * @return int Total count of translations
     */
    public function count_all_translations(): int {
        global $wpdb;
        
        $pattern = $wpdb->esc_like('_transient_fluentc_' . $this->blog_id . '_' . $this->version . '_translations_') . '%';
        
        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) 
                FROM {$wpdb->options} 
                WHERE option_name LIKE %s",
                $pattern
            )
        );
    }
}