<?php
/**
 * i18n Manager for FleekDash V2
 *
 * Handles integration between WordPress i18n and React frontend
 * Provides locale data and translations to the JavaScript application
 *
 * @package FleekDash V2
 * @since 2.1.0
 */

namespace FleekDash\Core;

defined('ABSPATH') || exit;

/**
 * Class I18nManager
 *
 * Manages internationalization for both PHP and React
 */
class I18nManager
{
    /**
     * Singleton instance.
     *
     * @var I18nManager|null
     */
    private static ?I18nManager $instance = null;

    /**
     * Text domain.
     */
    public const TEXT_DOMAIN = 'fleekdash';

    /**
     * WordPress official language packs directory suffix.
     */
    private const WORDPRESS_PLUGIN_LANG_DIR = '/languages/plugins/';

    /**
     * Downloaded FleekDash language packs directory suffix.
     */
    private const DOWNLOADED_PLUGIN_LANG_DIR = '/languages/plugins/fleekdash/';

    /**
     * Get singleton instance.
     */
    public static function getInstance(): I18nManager
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * Constructor.
     */
    private function __construct()
    {
        $this->initHooks();
    }

    /**
     * Initialize WordPress hooks.
     */
    private function initHooks(): void
    {
        \add_action('admin_enqueue_scripts', array($this, 'localizeScripts'), 999);
        \add_action('admin_enqueue_scripts', array($this, 'setScriptTranslations'), 1000);
    }

    /**
     * Whether the current hook belongs to FleekDash.
     *
     * @param string $hook Current admin page hook.
     */
    private function shouldLoadForHook(string $hook): bool
    {
        $page = isset($_GET['page']) ? (string) $_GET['page'] : '';

        return \strpos($hook, 'fleekdash') !== false
            || ($page !== '' && \strpos($page, 'fleekdash') !== false);
    }

    /**
     * Localize scripts with locale data.
     *
     * @param string $hook Current admin page hook.
     */
    public function localizeScripts(string $hook): void
    {
        if (!$this->shouldLoadForHook($hook)) {
            return;
        }

        \wp_localize_script('fleekdash-admin', 'fleekDashI18n', $this->getLocaleData());
    }

    /**
     * Set script translations for WordPress i18n.
     *
     * @param string $hook Current admin page hook.
     */
    public function setScriptTranslations(string $hook): void
    {
        if (!$this->shouldLoadForHook($hook)) {
            return;
        }

        \wp_set_script_translations(
            'fleekdash-admin',
            self::TEXT_DOMAIN,
            WP_CONTENT_DIR . '/languages/plugins'
        );
    }

    /**
     * Get locale data for JavaScript.
     *
     * @return array<string, mixed>
     */
    public function getLocaleData(): array
    {
        $locale = \get_user_locale();
        $language = \substr($locale, 0, 2);
        $currencySymbol = '$';
        $currencyPosition = 'left';
        $thousandsSeparator = ',';
        $decimalSeparator = '.';
        $numberOfDecimals = 2;

        if (\function_exists('get_woocommerce_currency_symbol')) {
            // @phpstan-ignore-next-line Function existence is checked above.
            $currencySymbol = \html_entity_decode(\get_woocommerce_currency_symbol());
            $currencyPosition = \get_option('woocommerce_currency_pos', 'left');
            $thousandsSeparator = \get_option('woocommerce_price_thousand_sep', ',');
            $decimalSeparator = \get_option('woocommerce_price_decimal_sep', '.');
            $numberOfDecimals = \get_option('woocommerce_price_num_decimals', 2);
        }

        $translations = $this->getTranslationsFromJson($locale);
        $textOverrides = $this->getTextOverrides($locale);

        return array(
            'locale' => $locale,
            'language' => $language,
            'textDomain' => self::TEXT_DOMAIN,
            'isRTL' => \is_rtl(),
            'dateFormat' => \get_option('date_format'),
            'timeFormat' => \get_option('time_format'),
            'startOfWeek' => \get_option('start_of_week'),
            'timezone' => \wp_timezone_string(),
            'currencySymbol' => $currencySymbol,
            'currencyPosition' => $currencyPosition,
            'thousandsSeparator' => $thousandsSeparator,
            'decimalSeparator' => $decimalSeparator,
            'numberOfDecimals' => $numberOfDecimals,
            'localeData' => $translations,
            'textOverrides' => $textOverrides,
        );
    }

    /**
     * Get language file path with multi-location support.
     *
     * Priority order (WordPress standard locations only):
     * 1. wp-content/languages/plugins/ (WordPress.org language packs)
     * 2. wp-content/languages/plugins/fleekdash/ (Downloaded from R2)
     *
     * NOTE: Plugin languages folder is NOT searched to avoid conflicts.
     * For development, use sync-dev-languages.php script to copy files to official location.
     *
     * @param string $locale Locale code.
     * @param string $extension File extension (po, mo, or js.json).
     */
    private function getLanguageFilePath(string $locale, string $extension): ?string
    {
        $filename = $extension === 'js.json'
            ? self::TEXT_DOMAIN . '-' . $locale . '-js.json'
            : self::TEXT_DOMAIN . '-' . $locale . '.' . $extension;

        $searchPaths = array(
            WP_CONTENT_DIR . self::WORDPRESS_PLUGIN_LANG_DIR . $filename,
            WP_CONTENT_DIR . self::DOWNLOADED_PLUGIN_LANG_DIR . $filename,
        );

        foreach ($searchPaths as $path) {
            if (\file_exists($path)) {
                return $path;
            }
        }

        return null;
    }

    /**
     * Get translations from JSON file.
     *
     * @param string $locale Locale code.
     * @return array<string, mixed>|null
     */
    private function getTranslationsFromJson(string $locale): ?array
    {
        $translations = null;
        $isEnglishLocale = $locale === 'en_US' || $locale === 'en';

        if (!$isEnglishLocale) {
            $jsonFile = $this->getLanguageFilePath($locale, 'js.json');
            if ($jsonFile !== null) {
                $jsonContent = \file_get_contents($jsonFile);
                if ($jsonContent !== false) {
                    $decoded = \json_decode($jsonContent, true);
                    if (\is_array($decoded) && $decoded !== array()) {
                        $translations = $decoded;
                    }
                }
            }
        }

        return $translations;
    }

    /**
     * Get available languages from all locations.
     *
     * Scans WordPress standard directories only:
     * 1. wp-content/languages/plugins/ (WordPress.org)
     * 2. wp-content/languages/plugins/fleekdash/ (Downloaded)
     *
     * NOTE: Plugin languages folder is NOT scanned to avoid conflicts.
     * For development, use sync-dev-languages.php script to copy files to official location.
     *
     * @return array<string, string>
     */
    public function getAvailableLanguages(): array
    {
        $available = array();
        $scanDirs = array(
            WP_CONTENT_DIR . self::WORDPRESS_PLUGIN_LANG_DIR,
            WP_CONTENT_DIR . self::DOWNLOADED_PLUGIN_LANG_DIR,
        );

        foreach ($scanDirs as $dir) {
            if (!\is_dir($dir)) {
                continue;
            }

            $files = \glob($dir . self::TEXT_DOMAIN . '-*.mo');
            if (!$files) {
                continue;
            }

            foreach ($files as $file) {
                $basename = \basename($file, '.mo');
                $locale = \str_replace(self::TEXT_DOMAIN . '-', '', $basename);

                if ($locale !== $basename && !isset($available[$locale])) {
                    $available[$locale] = $this->getLanguageName($locale);
                }
            }
        }

        $available['en_US'] = 'English';

        return $available;
    }

    /**
     * Get PO/MO file paths for installed languages (from filesystem scan).
     * Paths are discovered via glob, never constructed from user input (S2083 safe).
     * Includes MO-only languages (po => null) so validation and deletion use the same source.
     *
     * @return array<string, array{po: string|null, mo: string}>
     */
    public function getInstalledLanguageFilePaths(): array
    {
        $paths = array();
        $baseDir = \realpath(WP_CONTENT_DIR . '/languages');

        if ($baseDir === false) {
            return $paths;
        }

        $baseDir .= DIRECTORY_SEPARATOR;
        $scanDirs = array(
            WP_CONTENT_DIR . self::WORDPRESS_PLUGIN_LANG_DIR,
            WP_CONTENT_DIR . self::DOWNLOADED_PLUGIN_LANG_DIR,
        );

        foreach ($scanDirs as $dir) {
            if (!\is_dir($dir)) {
                continue;
            }

            $files = \glob($dir . self::TEXT_DOMAIN . '-*.mo');
            if (!$files) {
                continue;
            }

            foreach ($files as $moFile) {
                $this->addInstalledLanguageFilePath($moFile, $baseDir, $paths);
            }
        }

        return $paths;
    }

    /**
     * Add an installed language entry to the discovered paths collection.
     *
     * @param string                                    $moFile MO file path.
     * @param string                                    $baseDir Allowed base directory.
     * @param array<string, array{po: string|null, mo: string}> $paths Existing paths keyed by locale.
     */
    private function addInstalledLanguageFilePath(string $moFile, string $baseDir, array &$paths): void
    {
        $resolved = \realpath($moFile);
        if ($resolved === false || !\str_starts_with($resolved, $baseDir)) {
            return;
        }

        $basename = \basename($resolved, '.mo');
        $locale = \str_replace(self::TEXT_DOMAIN . '-', '', $basename);

        if ($locale === $basename || isset($paths[$locale])) {
            return;
        }

        $poPath = null;
        $poFile = \str_replace('.mo', '.po', $resolved);

        if (\file_exists($poFile)) {
            $poResolved = \realpath($poFile);
            if ($poResolved !== false && \str_starts_with($poResolved, $baseDir)) {
                $poPath = $poResolved;
            }
        }

        $paths[$locale] = array(
            'po' => $poPath,
            'mo' => $resolved,
        );
    }

    /**
     * Get language name from locale.
     *
     * @param string $locale Locale code.
     */
    private function getLanguageName(string $locale): string
    {
        $languages = array(
            'en_US' => 'English',
            'pt_BR' => 'Português (Brasil)',
            'pt_PT' => 'Português (Portugal)',
            'es_ES' => 'Español',
            'es_MX' => 'Español (México)',
            'fr_FR' => 'Français',
            'de_DE' => 'Deutsch',
            'it_IT' => 'Italiano',
            'nl_NL' => 'Nederlands',
            'ru_RU' => 'Русский',
            'ja' => '日本語',
            'zh_CN' => '简体中文',
            'zh_TW' => '繁體中文',
            'ko' => '한국어',
            'ar' => 'العربية',
        );

        return $languages[$locale] ?? $locale;
    }

    /**
     * Get translation statistics.
     *
     * @param string $locale Locale code.
     * @return array{translated: int, total: int, percentage: int}
     */
    public function getTranslationStats(string $locale): array
    {
        $stats = $this->getEmptyTranslationStats();
        $poFile = $this->getLanguageFilePath($locale, 'po');

        if ($poFile === null) {
            return $stats;
        }

        if (!\class_exists('PO')) {
            require_once ABSPATH . WPINC . '/pomo/po.php';
        }

        $po = new \PO();
        if (!$po->import_from_file($poFile)) {
            return $stats;
        }

        foreach ($po->entries as $entry) {
            if (!$this->shouldIncludePoEntry($entry)) {
                continue;
            }

            $stats['total']++;

            if ($this->isTranslatedPoEntry($entry)) {
                $stats['translated']++;
            }
        }

        $stats['percentage'] = $stats['total'] > 0
            ? (int) \round(($stats['translated'] / $stats['total']) * 100)
            : 100;

        return $stats;
    }

    /**
     * Get an empty translation stats structure.
     *
     * @return array{translated: int, total: int, percentage: int}
     */
    private function getEmptyTranslationStats(): array
    {
        return array(
            'translated' => 0,
            'total' => 0,
            'percentage' => 0,
        );
    }

    /**
     * Whether a PO entry should be considered in stats.
     *
     * @param object $entry PO entry object.
     */
    private function shouldIncludePoEntry(object $entry): bool
    {
        $singular = isset($entry->singular) ? (string) $entry->singular : '';
        $isMetadataEntry = $singular === ''
            || \strpos($singular, 'PO-Revision-Date') !== false
            || \strpos($singular, 'POT-Creation-Date') !== false
            || \strpos($singular, 'Language-Team') !== false
            || \strpos($singular, 'MIME-Version') !== false;

        return !$isMetadataEntry;
    }

    /**
     * Whether a PO entry contains a usable translation.
     *
     * @param object $entry PO entry object.
     */
    private function isTranslatedPoEntry(object $entry): bool
    {
        $translation = '';
        if (!empty($entry->translations[0])) {
            $translation = \trim((string) $entry->translations[0]);
        }

        $isFuzzy = !empty($entry->flags) && \in_array('fuzzy', $entry->flags, true);

        return $translation !== '' && !$isFuzzy;
    }

    /**
     * Export translations to JSON for JavaScript.
     *
     * @param string $locale Locale code.
     */
    public function exportToJson(string $locale): bool
    {
        $poFile = $this->getLanguageFilePath($locale, 'po');

        if ($poFile === null) {
            return false;
        }

        $poDir = \dirname($poFile);
        $jsonFile = $poDir . '/' . self::TEXT_DOMAIN . '-' . $locale . '-js.json';

        if (\function_exists('wp_get_pomo_file_data')) {
            $translations = \wp_get_pomo_file_data($poFile);

            if ($translations) {
                $jsonData = array(
                    'locale_data' => array(
                        self::TEXT_DOMAIN => $translations,
                    ),
                );

                $jsonContent = \wp_json_encode($jsonData);

                return (bool) \file_put_contents($jsonFile, $jsonContent);
            }
        }

        return false;
    }

    /**
     * Get text overrides from database (en_US only).
     *
     * @param string $locale Locale code.
     * @return array<string, string>|null
     */
    private function getTextOverrides(string $locale): ?array
    {
        $overrides = null;

        if ($locale === 'en_US') {
            $cacheKey = "text_overrides_{$locale}";
            $cached = \wp_cache_get($cacheKey, 'fleekdash');

            if ($cached !== false) {
                $overrides = \is_array($cached) ? $cached : null;
            } else {
                global $wpdb;

                $tableName = $wpdb->prefix . 'fleekdash_text_overrides';
                $tableExists = $wpdb->get_var("SHOW TABLES LIKE '{$tableName}'") === $tableName;

                if ($tableExists) {
                    $results = $wpdb->get_results(
                        $wpdb->prepare(
                            "SELECT original_text, custom_text FROM {$tableName} WHERE locale = %s",
                            $locale
                        ),
                        ARRAY_A
                    );

                    $overrides = array();

                    foreach ($results as $row) {
                        $overrides[$row['original_text']] = $row['custom_text'];
                    }

                    \wp_cache_set($cacheKey, $overrides, 'fleekdash', 300);
                }
            }
        }

        return $overrides;
    }

    /**
     * Clear translation cache.
     */
    public function clearCache(): void
    {
        \wp_cache_delete('translations', 'fleekdash');

        $locale = \get_user_locale();
        \wp_cache_delete("text_overrides_{$locale}", 'fleekdash');

        if (\function_exists('opcache_reset')) {
            \opcache_reset();
        }
    }
}
