<?php
/**
 * String Translation class - handles theme/plugin string translations
 * 
 * @package LangForge
 */

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

// IDE hints for autoloaded classes
if (false) {
    class LF_Core {}
}

class LF_String_Translation {
    
    private static $instance = null;
    private $strings_cache = [];
    private $strings_loaded = [];
    private $registered_check = [];      // md5 → true for strings already checked this request
    private $pending_registrations = []; // Queue for batch INSERT on shutdown
    private $auto_register_domains = null; // Cached allowed domains

    /**
 * Preload all translations for a language in one query (fix)
 */
    private function preload_strings($lang) {
        if (!empty($this->strings_loaded[$lang])) return;

        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';

        // Get all translated strings for this language in ONE query
        $rows = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            "SELECT string_domain, string_name, string_value FROM $table WHERE string_language = %s AND status = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
            $lang
        ), ARRAY_A);

        foreach ($rows as $row) {
            // Index by both name and value so lookups by either match
            $key_by_name = md5($row['string_domain'] . $row['string_name'] . $lang);
            $this->strings_cache[$key_by_name] = $row['string_value'];
            // Also index by value (for lookups where $text is the translated string itself)
            $key_by_value = md5($row['string_domain'] . $row['string_value'] . $lang);
            if (!isset($this->strings_cache[$key_by_value])) {
                $this->strings_cache[$key_by_value] = $row['string_value'];
            }
        }

        $this->strings_loaded[$lang] = true;
    }
    
    public static function instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct() {
        $this->init_hooks();
    }
    
    private function init_hooks() {
        // Filter gettext
        add_filter('gettext', [$this, 'filter_gettext'], 10, 3);
        add_filter('gettext_with_context', [$this, 'filter_gettext_with_context'], 10, 4);
        add_filter('ngettext', [$this, 'filter_ngettext'], 10, 5);

        // Filter WP options that need translation (blogname, blogdescription)
        add_filter('option_blogname', [$this, 'filter_option_string']);
        add_filter('option_blogdescription', [$this, 'filter_option_string']);

        // Filter taxonomy term names for translation
        add_filter('get_term', [$this, 'filter_term_name'], 10, 2);
        add_filter('get_the_terms', [$this, 'filter_terms_names'], 10, 3);
        add_filter('wp_get_object_terms', [$this, 'filter_object_terms_names'], 10, 4);

        // Batch-flush auto-registered strings at end of request
        add_action('shutdown', [$this, 'flush_pending_registrations']);
    }

    /**
 * Filter translatable WP options (blogname, blogdescription).
 * Looks up the option value in lf_strings table with domain 'WordPress'.
 */
    public function filter_option_string($value) {
        if (is_admin() && !wp_doing_ajax()) {
            return $value;
        }
        if (!class_exists('LF_Core')) return $value;

        $current_lang = LF_Core::instance()->get_current_language();
        if (!$current_lang) return $value;

        // Look up translation by value (the option value may be in any language)
        $this->preload_strings($current_lang);

        $cache_key = md5('WordPress' . $value . $current_lang);
        if (isset($this->strings_cache[$cache_key])) {
            return $this->strings_cache[$cache_key];
        }

        return $value;
    }
    
    /**
 * Translate a single term's name via string translation.
 */
    public function filter_term_name($term, $taxonomy) {
        if (is_admin() && !wp_doing_ajax()) return $term;
        if (!is_object($term) || empty($term->name)) return $term;
        $translated = $this->translate_taxonomy_string($term->name, $taxonomy);
        if ($translated !== $term->name) {
            $term->name = $translated;
        }
        return $term;
    }

    /**
 * Translate term names in get_the_terms results.
 */
    public function filter_terms_names($terms, $post_id, $taxonomy) {
        if (is_admin() && !wp_doing_ajax()) return $terms;
        if (!is_array($terms)) return $terms;
        foreach ($terms as $term) {
            if (!is_object($term) || empty($term->name)) continue;
            $translated = $this->translate_taxonomy_string($term->name, $term->taxonomy ?? $taxonomy);
            if ($translated !== $term->name) {
                $term->name = $translated;
            }
        }
        return $terms;
    }

    /**
 * Translate term names in wp_get_object_terms results.
 */
    public function filter_object_terms_names($terms, $object_ids, $taxonomies, $args) {
        if (is_admin() && !wp_doing_ajax()) return $terms;
        if (!is_array($terms)) return $terms;
        foreach ($terms as $term) {
            if (!is_object($term) || empty($term->name)) continue;
            $translated = $this->translate_taxonomy_string($term->name, $term->taxonomy ?? '');
            if ($translated !== $term->name) {
                $term->name = $translated;
            }
        }
        return $terms;
    }

    /**
 * Look up a taxonomy term name translation. Domain = 'taxonomy'.
 */
    private function translate_taxonomy_string($name, $taxonomy) {
        if (!class_exists('LF_Core')) return $name;
        $current_lang = LF_Core::instance()->get_current_language();
        if (!$current_lang) return $name;

        $this->preload_strings($current_lang);

        $cache_key = md5('taxonomy' . $name . $current_lang);
        if (isset($this->strings_cache[$cache_key])) {
            return $this->strings_cache[$cache_key];
        }
        return $name;
    }

    /**
 * Register a string for translation
 * @param string $string The string value
 * @param string $name The string identifier (usually same as string)
 * @param string $domain The text domain
 * @param string $source_lang The source language of the string
 */
    public function register_string($string, $name, $domain = 'default', $source_lang = null) {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        // Check if table exists
        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
        if (!$table_exists) {
            LF_Core::activate();
        }
        
        // Use provided source language or default
        if (!$source_lang) {
            $source_lang = LF_Core::instance()->get_default_language();
        }
        if (!$source_lang) {
            $source_lang = 'ru';
        }
        
        // Check if already exists for this source language
        $existing = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            "SELECT id FROM $table WHERE string_domain = %s AND string_name = %s AND string_language = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
            $domain, $name, $source_lang
        ));

        if (!$existing) {
            $wpdb->insert($table, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
                'string_domain' => $domain,
                'string_name' => $name,
                'string_value' => $string,
                'string_language' => $source_lang,
                'status' => 1,
            ]);
        }
        
        return $string;
    }
    
    /**
 * Translate a string based on its source language
 * @param string $string The original string
 * @param string $source_lang The source language of the string
 */
    public function translate_by_source($string, $source_lang) {
        $current_lang = LF_Core::instance()->get_current_language();
        
        // If current language matches source language, return original
        if ($current_lang === $source_lang) {
            return $string;
        }
        
        // Preload all translations for current language (once per request)
        if (!isset($this->strings_loaded[$current_lang])) {
            $this->preload_strings($current_lang);
        }

        // Check preloaded cache
        $cache_key = md5($string . $source_lang . $current_lang);
        if (isset($this->strings_cache[$cache_key])) {
            return $this->strings_cache[$cache_key];
        }

        return $string;
    }
    
    /**
 * Translate a registered string
 */
    public function translate($string, $name, $domain = 'default') {
        $current_lang = LF_Core::instance()->get_current_language();

        // Check cache first
        $cache_key = md5($domain . $name . $current_lang);
        if (isset($this->strings_cache[$cache_key])) {
            return $this->strings_cache[$cache_key];
        }

        // Always check DB — even for default language, user may have
        // overridden the value in String Translation admin page
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';

        $translation = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            "SELECT string_value FROM $table WHERE string_domain = %s AND string_name = %s AND string_language = %s AND status = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
            $domain, $name, $current_lang
        ));

        if ($translation) {
            $this->strings_cache[$cache_key] = $translation;
            return $translation;
        }

        // Fallback: return the shortcode/function default value
        return $string;
    }
    
    /**
 * Save string translation
 */
    public function save_translation($name, $domain, $language, $value) {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        // Check if exists
        $existing = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            "SELECT id FROM $table WHERE string_domain = %s AND string_name = %s AND string_language = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
            $domain, $name, $language
        ));

        if ($existing) {
            $wpdb->update( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
                $table,
                [
                    'string_value' => $value,
                    'status' => 1,
                ],
                ['id' => $existing]
            );
        } else {
            $wpdb->insert($table, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
                'string_domain' => $domain,
                'string_name' => $name,
                'string_value' => $value,
                'string_language' => $language,
                'status' => 1,
            ]);
        }
        
        // Clear cache
        $cache_key = md5($domain . $name . $language);
        unset($this->strings_cache[$cache_key]);
    }
    
    /**
 * Get all strings for a domain
 */
    public function get_strings($domain = 'default', $language = null) {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        if ($language) {
            return $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
                "SELECT * FROM $table WHERE string_domain = %s AND string_language = %s ORDER BY string_name", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
                $domain, $language
            ));
        }

        return $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            "SELECT * FROM $table WHERE string_domain = %s ORDER BY string_name, string_language", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
            $domain
        ));
    }
    
    /**
 * Get all registered domains
 */
    public function get_domains() {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        return $wpdb->get_col("SELECT DISTINCT string_domain FROM $table ORDER BY string_domain"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
    }
    
    /**
 * Get strings needing translation
 */
    public function get_untranslated_strings($domain = 'default', $target_language = null) {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        if (!$target_language) {
            $target_language = LF_Core::instance()->get_current_language();
        }
        
        $default_lang = LF_Core::instance()->get_default_language();
        
        // Get strings that exist in default language but not in target
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
        $sql = $wpdb->prepare(
            "SELECT s1.* FROM $table s1
             LEFT JOIN $table s2 ON s1.string_domain = s2.string_domain
                AND s1.string_name = s2.string_name
                AND s2.string_language = %s
             WHERE s1.string_domain = %s
                AND s1.string_language = %s
                AND s2.id IS NULL
             ORDER BY s1.string_name",
            $target_language, $domain, $default_lang
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        return $wpdb->get_results($sql); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; $sql already prepared above
    }
    
    /**
 * Filter gettext for registered strings.
 * Also auto-registers strings from allowed domains on first encounter (frontend only).
 */
    public function filter_gettext($translation, $text, $domain) {
        // Don't override translations in admin — let WordPress .mo files handle it.
        // Lang Forge's string translations are for the frontend only.
        if (is_admin() && !wp_doing_ajax()) {
            return $translation;
        }

        $current_lang = LF_Core::instance()->get_current_language();
        if (!$current_lang) return $translation;

        // Preload all strings for this language in one query 
        $this->preload_strings($current_lang);

        $cache_key = md5($domain . $text . $current_lang);
        if (isset($this->strings_cache[$cache_key])) {
            $cached = $this->strings_cache[$cache_key];
            return ($cached !== $text) ? $cached : $translation;
        }

        // String not in DB — auto-register before caching the miss
        if (!is_admin() && !wp_doing_ajax() && !wp_doing_cron()) {
            $this->maybe_auto_register($text, $domain);
        }

        // Cache miss — no SQL needed on subsequent calls
        $this->strings_cache[$cache_key] = $translation;
        return $translation;
    }

    // =========================================================================
    // Auto-registration of strings from gettext filter
    // =========================================================================

    /**
 * Queue a string for auto-registration if it belongs to an allowed domain.
 * Does NOT hit the database — strings are batched and flushed on shutdown.
 */
    private function maybe_auto_register($text, $domain) {
        // Skip empty, too short, or numeric strings
        if (empty($text) || strlen($text) < 2 || is_numeric($text)) {
            return;
        }

        // Skip WP Core and Forge Suite domains — too many strings, not useful
        $skip_domains = ['default', '', 'langforge', 'seoforge', 'formforge', 'fieldforge'];
        if (in_array($domain, $skip_domains, true)) {
            return;
        }

        // In-memory dedup — don't process the same string twice per request
        $dedup_key = md5($domain . $text);
        if (isset($this->registered_check[$dedup_key])) {
            return;
        }
        $this->registered_check[$dedup_key] = true;

        // Check if string is already in preloaded cache (any language)
        // If it's in the default-language preload, it's already registered
        $default_lang = LF_Core::instance()->get_default_language();
        $this->preload_strings($default_lang);
        $default_key = md5($domain . $text . $default_lang);
        if (isset($this->strings_cache[$default_key])) {
            return;
        }

        // Check allowed domains
        if (!$this->is_allowed_domain($domain)) {
            return;
        }

        // Add to pending queue — will be batch-inserted on shutdown
        $this->pending_registrations[] = [
            'domain' => $domain,
            'name'   => mb_substr($text, 0, 160), // Respect column max length
            'value'  => $text,
        ];
    }

    /**
 * Check if a text domain is in the auto-register allow list.
 * Default: active theme's text domain.
 */
    private function is_allowed_domain($domain) {
        if ($this->auto_register_domains === null) {
            $saved = get_option('lf_auto_register_domains', '');
            if (!empty($saved)) {
                $this->auto_register_domains = array_filter(array_map('trim', explode("\n", $saved)));
            } else {
                // Default: active theme's text domain
                $theme_domain = wp_get_theme()->get('TextDomain');
                $this->auto_register_domains = $theme_domain ? [$theme_domain] : [];
            }
        }

        return in_array($domain, $this->auto_register_domains, true);
    }

    /**
 * Batch INSERT IGNORE all pending registrations. Called on shutdown hook.
 * Uses INSERT IGNORE to silently skip duplicates (UNIQUE KEY on table).
 */
    public function flush_pending_registrations() {
        if (empty($this->pending_registrations)) {
            return;
        }

        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        $default_lang = LF_Core::instance()->get_default_language() ?: 'ru';

        // Build batch INSERT IGNORE
        $values = [];
        $placeholders = [];
        foreach ($this->pending_registrations as $reg) {
            $placeholders[] = '(%s, %s, %s, %s, 1)';
            $values[] = $reg['domain'];
            $values[] = $reg['name'];
            $values[] = $reg['value'];
            $values[] = $default_lang;
        }

        if (!empty($placeholders)) {
            $sql = "INSERT IGNORE INTO $table (string_domain, string_name, string_value, string_language, status) VALUES "
                 . implode(', ', $placeholders);
            $wpdb->query($wpdb->prepare($sql, $values)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; %s placeholders prepared
        }

        $this->pending_registrations = [];
    }
    
    public function filter_gettext_with_context($translation, $text, $context, $domain) {
        return $this->filter_gettext($translation, $text, $domain);
    }
    
    public function filter_ngettext($translation, $single, $plural, $number, $domain) {
        $text = $number == 1 ? $single : $plural;
        return $this->filter_gettext($translation, $text, $domain);
    }
    
    /**
 * Delete a string
 */
    public function delete_string($name, $domain = 'default') {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        $wpdb->delete($table, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            'string_domain' => $domain,
            'string_name' => $name,
        ]);
    }
    
    /**
 * Delete all strings by domain
 */
    public function delete_domain($domain) {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        return $wpdb->delete($table, [ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; direct query intended.
            'string_domain' => $domain,
        ]);
    }
    
    /**
 * Import strings from PO file
 */
    public function import_from_po($file_path, $domain, $language) {
        if (!file_exists($file_path)) {
            return false;
        }
        
        $content = file_get_contents($file_path);
        $entries = $this->parse_po($content);
        
        foreach ($entries as $entry) {
            if (!empty($entry['msgid']) && !empty($entry['msgstr'])) {
                $this->save_translation(
                    $entry['msgid'],
                    $domain,
                    $language,
                    $entry['msgstr']
                );
            }
        }
        
        return count($entries);
    }
    
    private function parse_po($content) {
        $entries = [];
        $current = ['msgid' => '', 'msgstr' => ''];
        $current_key = '';
        
        $lines = explode("\n", $content);
        
        foreach ($lines as $line) {
            $line = trim($line);
            
            if (empty($line) || $line[0] === '#') {
                if (!empty($current['msgid'])) {
                    $entries[] = $current;
                    $current = ['msgid' => '', 'msgstr' => ''];
                }
                continue;
            }
            
            if (strpos($line, 'msgid ') === 0) {
                $current['msgid'] = $this->extract_po_string($line);
                $current_key = 'msgid';
            } elseif (strpos($line, 'msgstr ') === 0) {
                $current['msgstr'] = $this->extract_po_string($line);
                $current_key = 'msgstr';
            } elseif ($line[0] === '"' && $current_key) {
                $current[$current_key] .= $this->extract_po_string($line);
            }
        }
        
        if (!empty($current['msgid'])) {
            $entries[] = $current;
        }
        
        return $entries;
    }
    
    private function extract_po_string($line) {
        preg_match('/"(.*)"/s', $line, $matches);
        if (isset($matches[1])) {
            return stripcslashes($matches[1]);
        }
        return '';
    }
}
