<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\I18n;

defined('ABSPATH') || exit;

class PluginTranslationManager
{
    public const OPTION = 'apd_plugin_translation_locales';
    private const DOMAIN = 'asuka-product-designer';

    /** @var array<string,array<string,string>> */
    private static array $runtimeCache = [];

    public function getLocaleTranslations(string $locale) : array
    {
        $normalized = JsTranslationManager::normalizeLocale($locale);
        $option = get_option(self::OPTION, []);
        $locales = is_array($option) && is_array($option['locales'] ?? null) ? $option['locales'] : [];
        $translations = is_array($locales[$normalized]['translations'] ?? null)
            ? $locales[$normalized]['translations']
            : [];

        return $this->sanitizeStoredTranslations($translations);
    }

    public function saveLocale(string $locale, array $translations) : void
    {
        $normalized = JsTranslationManager::normalizeLocale($locale);
        if ($normalized === '' || $normalized === 'en') {
            return;
        }

        $allowed = array_fill_keys(array_column($this->getEditorRows($normalized), 'key'), true);
        $sanitized = [];
        foreach ($translations as $key => $value) {
            $key = sanitize_key((string) $key);
            if (! isset($allowed[$key])) {
                continue;
            }
            $text = sanitize_textarea_field((string) $value);
            if ($text !== '') {
                $sanitized[$key] = $text;
            }
        }
        ksort($sanitized, SORT_NATURAL | SORT_FLAG_CASE);

        $option = get_option(self::OPTION, []);
        if (! is_array($option)) {
            $option = [];
        }
        if (! isset($option['locales']) || ! is_array($option['locales'])) {
            $option['locales'] = [];
        }
        $option['locales'][$normalized] = [
            'translations' => $sanitized,
            'updated_at' => current_time('mysql'),
        ];
        update_option(self::OPTION, $option, false);
        self::$runtimeCache = [];
    }

    public function deleteLocale(string $locale) : void
    {
        $normalized = JsTranslationManager::normalizeLocale($locale);
        $option = get_option(self::OPTION, []);
        if (! is_array($option) || ! is_array($option['locales'] ?? null)) {
            return;
        }
        unset($option['locales'][$normalized]);
        update_option(self::OPTION, $option, false);
        self::$runtimeCache = [];
    }

    public function getEditorRows(string $locale) : array
    {
        $translations = $this->getLocaleTranslations($locale);
        $rows = [];
        foreach ($this->scanPluginStrings() as $entry) {
            $key = self::key((string) $entry['source'], (string) $entry['context']);
            $rows[] = [
                'key' => $key,
                'namespace' => (string) $entry['namespace'],
                'default' => (string) $entry['source'],
                'context' => (string) $entry['context'],
                'files' => (array) $entry['files'],
                'translation' => (string) ($translations[$key] ?? ''),
                'translated' => isset($translations[$key]) && $translations[$key] !== '',
            ];
        }

        usort($rows, static fn(array $left, array $right) : int => [
            $left['namespace'],
            $left['default'],
            $left['context'],
        ] <=> [
            $right['namespace'],
            $right['default'],
            $right['context'],
        ]);

        return $rows;
    }

    public function getLocaleStats(string $locale) : array
    {
        $rows = $this->getEditorRows($locale);
        $normalized = JsTranslationManager::normalizeLocale($locale);
        if ($normalized === 'en') {
            return [
                'total' => count($rows),
                'translated' => count($rows),
                'missing' => 0,
            ];
        }

        $parts = explode('_', $normalized);
        $language = strtolower((string) ($parts[0] ?? ''));
        $effective = array_replace(
            $language !== '' && $language !== $normalized ? $this->getLocaleTranslations($language) : [],
            $this->getLocaleTranslations($normalized)
        );
        $translated = count(array_filter($rows, static fn(array $row) : bool => isset($effective[$row['key']]) && $effective[$row['key']] !== ''));
        return [
            'total' => count($rows),
            'translated' => $translated,
            'missing' => max(0, count($rows) - $translated),
        ];
    }

    public function filterGettext(string $translation, string $text, string $domain) : string
    {
        return $domain === self::DOMAIN ? $this->translate($text, '', $translation) : $translation;
    }

    public function filterGettextWithContext(string $translation, string $text, string $context, string $domain) : string
    {
        return $domain === self::DOMAIN ? $this->translate($text, $context, $translation) : $translation;
    }

    public function filterNgettext(string $translation, string $single, string $plural, int|float $number, string $domain) : string
    {
        if ($domain !== self::DOMAIN) {
            return $translation;
        }
        return $this->translate((float) $number === 1.0 ? $single : $plural, '', $translation);
    }

    public function filterNgettextWithContext(string $translation, string $single, string $plural, int|float $number, string $context, string $domain) : string
    {
        if ($domain !== self::DOMAIN) {
            return $translation;
        }
        return $this->translate((float) $number === 1.0 ? $single : $plural, $context, $translation);
    }

    public static function key(string $source, string $context = '') : string
    {
        return 'wp_' . substr(hash('sha256', $context . "\004" . $source), 0, 20);
    }

    private function translate(string $source, string $context, string $fallback) : string
    {
        $locale = JsTranslationManager::normalizeLocale(determine_locale() ?: get_locale());
        if ($locale === '' || $locale === 'en') {
            return $fallback;
        }

        $key = self::key($source, $context);
        foreach (array_unique([$locale, strtolower((string) strtok($locale, '_'))]) as $candidate) {
            $translations = $this->runtimeTranslations($candidate);
            if (isset($translations[$key]) && $translations[$key] !== '') {
                return $translations[$key];
            }
        }

        // The original translation may already come from WordPress, a .mo file or Loco Translate.
        return $fallback;
    }

    private function runtimeTranslations(string $locale) : array
    {
        if (! isset(self::$runtimeCache[$locale])) {
            self::$runtimeCache[$locale] = $this->getLocaleTranslations($locale);
        }
        return self::$runtimeCache[$locale];
    }

    private function sanitizeStoredTranslations(array $translations) : array
    {
        $sanitized = [];
        foreach ($translations as $key => $value) {
            $key = sanitize_key((string) $key);
            $text = sanitize_textarea_field((string) $value);
            if (str_starts_with($key, 'wp_') && $text !== '') {
                $sanitized[$key] = $text;
            }
        }
        return $sanitized;
    }

    private function scanPluginStrings() : array
    {
        static $entries = null;
        if ($entries !== null) {
            return $entries;
        }

        $entries = [];
        $files = [ASUKA_PRODUCT_DESIGNER_FILE];
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
            ASUKA_PRODUCT_DESIGNER_PATH . 'includes',
            \FilesystemIterator::SKIP_DOTS
        ));
        foreach ($iterator as $file) {
            if ($file->isFile() && strtolower($file->getExtension()) === 'php') {
                $files[] = $file->getPathname();
            }
        }

        foreach ($files as $filePath) {
            $source = file_get_contents($filePath);
            if (! is_string($source)) {
                continue;
            }
            $relative = ltrim(str_replace('\\', '/', str_replace(ASUKA_PRODUCT_DESIGNER_PATH, '', $filePath)), '/');
            $pathParts = array_values(array_filter(explode('/', $relative), 'strlen'));
            $namespace = ($pathParts[0] ?? '') === 'includes'
                ? (string) ($pathParts[1] ?? 'Plugin')
                : (string) ($pathParts[0] ?? 'Plugin');
            foreach ($this->extractStringsFromPhp($source) as $item) {
                $signature = (string) $item['context'] . "\004" . (string) $item['source'];
                if (! isset($entries[$signature])) {
                    $entries[$signature] = [
                        'source' => (string) $item['source'],
                        'context' => (string) $item['context'],
                        'namespace' => preg_replace('/\.php$/i', '', $namespace) ?: 'Plugin',
                        'files' => [],
                    ];
                }
                $entries[$signature]['files'][] = $relative;
                $entries[$signature]['files'] = array_values(array_unique($entries[$signature]['files']));
            }
        }

        return array_values($entries);
    }

    private function extractStringsFromPhp(string $source) : array
    {
        $functions = [
            '__' => ['source' => 0], '_e' => ['source' => 0],
            'esc_html__' => ['source' => 0], 'esc_html_e' => ['source' => 0],
            'esc_attr__' => ['source' => 0], 'esc_attr_e' => ['source' => 0],
            '_x' => ['source' => 0, 'context' => 1], '_ex' => ['source' => 0, 'context' => 1],
            'esc_html_x' => ['source' => 0, 'context' => 1], 'esc_attr_x' => ['source' => 0, 'context' => 1],
            '_n' => ['source' => 0, 'plural' => 1],
            '_nx' => ['source' => 0, 'plural' => 1, 'context' => 3],
        ];
        $tokens = token_get_all($source);
        $found = [];

        for ($index = 0, $count = count($tokens); $index < $count; $index++) {
            $token = $tokens[$index];
            if (! is_array($token) || $token[0] !== T_STRING) {
                continue;
            }
            $name = strtolower((string) $token[1]);
            if (! isset($functions[$name])) {
                continue;
            }
            $cursor = $index + 1;
            while ($cursor < $count && is_array($tokens[$cursor]) && in_array($tokens[$cursor][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                $cursor++;
            }
            if (($tokens[$cursor] ?? null) !== '(') {
                continue;
            }

            $arguments = $this->readLiteralArguments($tokens, $cursor + 1);
            $definition = $functions[$name];
            $sourceText = $arguments[$definition['source']] ?? null;
            $context = isset($definition['context']) ? ($arguments[$definition['context']] ?? '') : '';
            if (! is_string($sourceText) || $sourceText === '' || ! is_string($context)) {
                continue;
            }
            $found[] = ['source' => $sourceText, 'context' => $context];
            if (isset($definition['plural'])) {
                $plural = $arguments[$definition['plural']] ?? null;
                if (is_string($plural) && $plural !== '') {
                    $found[] = ['source' => $plural, 'context' => $context];
                }
            }
        }

        return $found;
    }

    private function readLiteralArguments(array $tokens, int $cursor) : array
    {
        $arguments = [];
        $depth = 1;
        $argumentIndex = 0;
        $literal = null;
        $valid = true;
        $count = count($tokens);

        for (; $cursor < $count && $depth > 0; $cursor++) {
            $token = $tokens[$cursor];
            if ($token === '(' || $token === '[' || $token === '{') {
                $depth++;
                if ($depth === 2) $valid = false;
                continue;
            }
            if ($token === ')' || $token === ']' || $token === '}') {
                $depth--;
                if ($depth === 0) {
                    $arguments[$argumentIndex] = $valid ? $literal : null;
                }
                continue;
            }
            if ($token === ',' && $depth === 1) {
                $arguments[$argumentIndex++] = $valid ? $literal : null;
                $literal = null;
                $valid = true;
                continue;
            }
            if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                continue;
            }
            if ($depth === 1 && is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING && $literal === null) {
                $literal = $this->decodePhpString((string) $token[1]);
                continue;
            }
            if ($depth === 1) {
                $valid = false;
            }
        }

        return $arguments;
    }

    private function decodePhpString(string $literal) : string
    {
        $quote = $literal[0] ?? '';
        $body = substr($literal, 1, -1);
        return $quote === "'"
            ? str_replace(["\\\\", "\\'"], ["\\", "'"], $body)
            : stripcslashes($body);
    }
}
