<?php
/**
 * Translation Engine Class
 * Core translation logic with batch processing and error handling
 */

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

class AITRFOEL_Translation_Engine {
    private static $instance = null;
    
    /**
     * Batch size for API calls — smaller batches = more frequent time-budget checks = reliable partial saves
     */
    private $batch_size = 20;
    
    /**
     * Rate limit delay (milliseconds) - only used for fallback individual calls
     */
    private $rate_limit_delay = 10;
    
    /**
     * Last translation statistics
     */
    private $last_translation_stats = [];
    
    /**
     * HTML placeholders storage - maps original text hash to placeholders
     */
    private $html_placeholders_storage = [];

    /**
     * Whether cache usage is enabled via settings
     */
    private $cache_enabled = true;

    public static function get_instance() {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        $this->batch_size = get_option('aitrfoel_batch_size', 20);
        $this->cache_enabled = (bool) get_option('aitrfoel_cache_enabled', true);
    }

    /**
     * Count words in text using Unicode-aware regex.
     * Unlike PHP's str_word_count(), this correctly handles apostrophes
     * inside words (e.g. French "l'eau", "d'une" count as 1 word each).
     *
     * @param string $text Plain text (should be HTML-stripped before calling)
     * @return int Word count
     */
    public static function count_words($text) {
        if (empty($text)) {
            return 0;
        }
        $count = preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\x{2019}\x{2018}\'-]*/u', $text, $matches);
        return $count ?: 0;
    }

    /**
     * Main translation function
     * Supports Elementor, Gutenberg, and Classic Editor content
     * 
     * @param int $page_id Original page ID
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param array $strings_to_translate Selected strings for translation (optional)
     * @return int|WP_Error New page ID or error
     */
    public function translate_page($page_id, $target_lang, $source_lang = 'auto', $strings_to_translate = []) {
        // Track translation start time for analytics
        $translation_start_time = microtime(true);
        
        // Detect content type
        $content_detector = AITRFOEL_Content_Detector::get_instance();
        $content_type = $content_detector->detect($page_id);
        
        AITRFOEL_Logger::log(
            sprintf('Content type detected: %s for page %d', $content_type, $page_id),
            'info',
            $page_id
        );
        
        // WooCommerce structural pages (cart / checkout / my-account / terms) hold only a
        // block/shortcode, so there is nothing to translate in the body — their UI strings
        // come from WooCommerce core translations via locale switching. We still want a
        // linked, published shell so the page registers in the admin, routes under the
        // language prefix, and the wc page-id option filters can swap to it.
        $is_wc_structural = $this->is_wc_structural_page($page_id);

        // Route to appropriate translation method based on content type
        $result = null;
        switch ($content_type) {
            case AITRFOEL_Content_Detector::TYPE_ELEMENTOR:
                $result = $this->translate_elementor_page($page_id, $target_lang, $source_lang, $strings_to_translate, $translation_start_time);
                break;

            case AITRFOEL_Content_Detector::TYPE_GUTENBERG:
                $result = $this->translate_gutenberg_page($page_id, $target_lang, $source_lang, $translation_start_time);
                break;

            case AITRFOEL_Content_Detector::TYPE_CLASSIC:
                $result = $this->translate_classic_page($page_id, $target_lang, $source_lang, $translation_start_time);
                break;

            case AITRFOEL_Content_Detector::TYPE_WOOCOMMERCE:
                $result = $this->translate_woocommerce_product($page_id, $target_lang, $source_lang, $translation_start_time);
                break;

            default:
                if ($is_wc_structural) {
                    return $this->translate_wc_structural_page($page_id, $target_lang, $source_lang, $translation_start_time);
                }
                $this->track_translation_failure('unsupported_content', 'Unsupported content type', $translation_start_time);
                return new WP_Error(
                    'unsupported_content',
                    __('This page does not contain any translatable content. Please ensure the page has content created with Elementor, Gutenberg (Block Editor), or Classic Editor.', 'ai-translator-for-elementor-and-polylang')
                );
        }

        // For WooCommerce structural pages, if normal translation found nothing to translate,
        // still create the linked shell so the page registers and routes (UI via locale switch).
        if ($is_wc_structural && is_wp_error($result)
            && in_array($result->get_error_code(), ['no_translatable_content', 'no_content'], true)) {
            AITRFOEL_Logger::log(
                sprintf('WooCommerce structural page %d has no translatable body; creating linked shell.', $page_id),
                'info', $page_id
            );
            return $this->translate_wc_structural_page($page_id, $target_lang, $source_lang, $translation_start_time);
        }

        // After a page is translated, optionally translate the Elementor templates it
        // relies on (embedded [elementor-template] / Template widget, plus theme-builder
        // singles/headers/footers) into the same language, so non-technical users don't
        // have to know a page pulls in an extra template. Opt-in; uses extra words.
        if (!is_wp_error($result) && get_post_type($page_id) !== 'elementor_library') {
            $this->maybe_auto_translate_templates($page_id, $target_lang, $source_lang);
        }

        return $result;
    }

    /**
     * If auto-translate-templates is enabled, translate every untranslated Elementor
     * template into $target_lang (covers embeds and theme-builder templates). Guarded
     * against re-entry so translating a template never recurses.
     *
     * @param int    $page_id      The page that was just translated (for logging).
     * @param string $target_lang
     * @param string $source_lang
     */
    private function maybe_auto_translate_templates($page_id, $target_lang, $source_lang) {
        static $running = false;
        if ($running) {
            return;
        }
        if (!get_option('aitrfoel_auto_translate_templates', false)) {
            return;
        }
        if (!post_type_exists('elementor_library')) {
            return;
        }

        $template_ids = $this->get_untranslated_template_ids($target_lang);
        if (empty($template_ids)) {
            return;
        }

        $running = true;
        $done = 0;
        foreach ($template_ids as $tid) {
            try {
                $res = $this->translate_page((int) $tid, $target_lang, $source_lang);
                if (!is_wp_error($res)) {
                    $done++;
                }
            } catch (\Throwable $e) {
                if (class_exists('AITRFOEL_Logger')) {
                    AITRFOEL_Logger::log('Auto template translation failed for #' . $tid . ': ' . $e->getMessage(), 'warning', $tid);
                }
            }
        }
        $running = false;

        if ($done > 0 && class_exists('AITRFOEL_Logger')) {
            AITRFOEL_Logger::log(
                sprintf('Auto-translated %d Elementor template(s) to %s after translating page %d', $done, $target_lang, $page_id),
                'success', $page_id
            );
        }
    }

    /**
     * IDs of published default-language Elementor templates that have no translation in
     * $target_lang yet. Excludes templates that are themselves translations.
     *
     * @param string $target_lang
     * @return int[]
     */
    public function get_untranslated_template_ids($target_lang) {
        // Elementor templates, plus JetEngine listing items (post type 'jet-engine')
        // which are Elementor-built templates carrying static text (labels) that need
        // translating too. Extensible via filter for other builder template types.
        $template_types = apply_filters('aitrfoel_template_post_types', ['elementor_library', 'jet-engine']);
        $template_types = array_values(array_filter((array) $template_types, 'post_type_exists'));
        if (empty($template_types)) {
            return [];
        }
        $mm = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null;

        $all = get_posts([
            'post_type'        => $template_types,
            'post_status'      => 'publish',
            'posts_per_page'   => -1,
            'fields'           => 'ids',
            'no_found_rows'    => true,
            'suppress_filters' => true,
        ]);

        $ids = [];
        foreach ($all as $tid) {
            $tid = (int) $tid;
            // Skip templates that are themselves a translation.
            $src = (int) get_post_meta($tid, '_aitrfoel_source_page', true);
            if ($src && $src !== $tid) {
                continue;
            }
            // Skip if a translation already exists in the target language.
            if ($mm && method_exists($mm, 'get_post_translations')) {
                $tr = $mm->get_post_translations($tid);
                if (is_array($tr) && isset($tr[$target_lang])) {
                    continue;
                }
            }
            $ids[] = $tid;
        }
        return $ids;
    }

    /**
     * Approximate translatable word count across all untranslated templates for a
     * language — used to warn the user before a bulk template translation.
     *
     * @param string $target_lang
     * @return array{count:int,words:int}
     */
    /**
     * Find every Elementor template that is actually referenced (embedded) somewhere,
     * so templates without display conditions but used via the Template/Loop widgets
     * or an [elementor-template] shortcode are recognised as "used" rather than
     * buried as optional. Scans _elementor_data and post_content; cached briefly.
     *
     * @return int[] Referenced template IDs.
     */
    public function get_referenced_template_ids() {
        $cached = get_transient('aitrfoel_referenced_template_ids');
        if (is_array($cached)) {
            return $cached;
        }

        global $wpdb;
        $ids = [];

        // Elementor widgets that embed a template store "template_id":"123" (Template
        // widget, Loop Grid, Theme Builder, Posts/Portfolio with a skin, etc.).
        $data_rows = $wpdb->get_col(
            "SELECT meta_value FROM {$wpdb->postmeta}
             WHERE meta_key = '_elementor_data'
             AND (meta_value LIKE '%template_id%' OR meta_value LIKE '%elementor-template%')"
        );
        foreach ((array) $data_rows as $data) {
            if (preg_match_all('/"(?:template_id|selected_template|listing_id)"\s*:\s*"?(\d+)"?/', (string) $data, $m)) {
                foreach ($m[1] as $id) { $ids[(int) $id] = true; }
            }
            if (preg_match_all('/elementor-template\s+id=\\\\?["\']?(\d+)/', (string) $data, $m2)) {
                foreach ($m2[1] as $id) { $ids[(int) $id] = true; }
            }
        }

        // [elementor-template id="123"] shortcodes embedded in post content.
        $content_rows = $wpdb->get_col(
            "SELECT post_content FROM {$wpdb->posts}
             WHERE post_content LIKE '%elementor-template%' AND post_status NOT IN ('trash','auto-draft')"
        );
        foreach ((array) $content_rows as $content) {
            if (preg_match_all('/elementor-template\s+id=["\']?(\d+)/', (string) $content, $m)) {
                foreach ($m[1] as $id) { $ids[(int) $id] = true; }
            }
        }

        $result = array_values(array_map('intval', array_keys($ids)));
        set_transient('aitrfoel_referenced_template_ids', $result, 5 * MINUTE_IN_SECONDS);
        return $result;
    }

    public function get_untranslated_templates_summary($target_lang) {
        $ids = $this->get_untranslated_template_ids($target_lang);
        $words = 0;
        foreach ($ids as $tid) {
            $raw = get_post_meta($tid, '_elementor_data', true);
            if (!empty($raw)) {
                $words += self::count_words(wp_strip_all_tags((string) $raw));
            }
        }
        return ['count' => count($ids), 'words' => $words];
    }

    /**
     * Whether the page is a WooCommerce structural page (cart / checkout / my-account / terms).
     * The Shop page is intentionally excluded — it has its own handling (classic shell + the
     * product-archive query conversion).
     *
     * @param int $page_id
     * @return bool
     */
    private function is_wc_structural_page($page_id) {
        if (!function_exists('wc_get_page_id')) {
            return false;
        }
        $page_id = (int) $page_id;
        foreach (['cart', 'checkout', 'myaccount', 'terms'] as $key) {
            $pid = (int) wc_get_page_id($key);
            if ($pid > 0 && $pid === $page_id) {
                return true;
            }
        }
        return false;
    }

    /**
     * Create a linked, published shell translation for a WooCommerce structural page.
     * The body (a WooCommerce block/shortcode) is copied as-is; only the title is translated.
     * The visible UI strings are rendered by WooCommerce core translations via locale switching.
     *
     * @param int    $page_id
     * @param string $target_lang
     * @param string $source_lang
     * @param float  $translation_start_time
     * @return int|WP_Error new page ID or error
     */
    private function translate_wc_structural_page($page_id, $target_lang, $source_lang, $translation_start_time) {
        $post = get_post($page_id);
        if (!$post) {
            $this->track_translation_failure('no_content', 'WooCommerce structural page not found', $translation_start_time);
            return new WP_Error('no_content', __('Page not found.', 'ai-translator-for-elementor-and-polylang'));
        }

        // Translate just the title; copy the block/shortcode body unchanged.
        $api              = AITRFOEL_OpenAI_API::get_instance();
        $translated_title = $api->translate_title($post->post_title, $target_lang, $source_lang);
        if (is_wp_error($translated_title) || empty($translated_title)) {
            $translated_title = null; // create_translated_page_generic falls back to "Title (LANG)"
        }

        $detected_lang = ($source_lang === 'auto') ? 'en' : $source_lang;

        $new_page_id = $this->create_translated_page_generic(
            $page_id,
            (string) $post->post_content,
            $target_lang,
            $detected_lang,
            'woocommerce',
            $translated_title
        );
        if (is_wp_error($new_page_id)) {
            return $new_page_id;
        }

        // Structural WooCommerce pages must be published: the pre_option page-id filters and
        // routing only swap to a *published* translation. A draft would silently break the
        // localized cart/checkout/account routing.
        if (get_post_status($new_page_id) !== 'publish') {
            wp_update_post(['ID' => $new_page_id, 'post_status' => 'publish']);
            clean_post_cache($new_page_id);
        }

        AITRFOEL_Logger::log(
            sprintf('WooCommerce structural page %d → %d (%s): linked shell created; UI strings via locale switching.',
                $page_id, $new_page_id, $target_lang),
            'success', $page_id, $new_page_id
        );

        if (class_exists('AITRFOEL_Translation_History')) {
            AITRFOEL_Translation_History::record([
                'original_post_id'   => $page_id,
                'translated_post_id' => $new_page_id,
                'source_lang'        => $detected_lang,
                'target_lang'        => $target_lang,
                'content_type'       => 'woocommerce',
                'word_count'         => 0,
                'status'             => 'success',
                'duration'           => round(microtime(true) - $translation_start_time, 2),
            ]);
        }

        return $new_page_id;
    }

    // =========================================================================
    // ASYNC TRANSLATION  (submit → poll, no blocking HTTP in the AJAX handler)
    // =========================================================================

    /**
     * Async Phase 1: Parse page content, check word limit, submit uncached strings
     * to the proxy async endpoint.  Returns immediately (< 2 s).
     *
     * @param int    $page_id     Original page ID
     * @param string $target_lang Target language code
     * @param string $source_lang Source language code or 'auto'
     * @return string|WP_Error
     *   proxy_job_id string   — job enqueued on proxy; call poll_page_async() to check
     *   'sync_required'       — Elementor content; caller must use synchronous path
     *   'cache_hit:{key}'     — all strings cached; page created; retrieve ID via get_transient($key)
     *   WP_Error              — on failure
     */
    public function submit_page_async($page_id, $target_lang, $source_lang = 'auto') {
        $content_type = AITRFOEL_Content_Detector::get_instance()->detect($page_id);

        // Elementor has complex inline placeholder handling. It stays on the proven sync
        // path by default; the async (submit→poll) path is opt-in via a setting and gated
        // here so existing behavior is unchanged unless explicitly enabled.
        if ($content_type === AITRFOEL_Content_Detector::TYPE_ELEMENTOR) {
            if (!get_option('aitrfoel_elementor_async_enabled', false)) {
                return 'sync_required';
            }
            $submitted = $this->submit_elementor_async($page_id, $target_lang, $source_lang);
            // On any failure, fall back to the synchronous path rather than erroring out.
            if (is_wp_error($submitted)) {
                AITRFOEL_Logger::log('Elementor async submit failed, falling back to sync: ' . $submitted->get_error_message(), 'warning', $page_id);
                return 'sync_required';
            }
            return $submitted;
        }

        $parse = $this->parse_page_content_for_async($page_id, $content_type, $source_lang);
        if (is_wp_error($parse)) {
            return $parse;
        }

        $strings = $parse['strings'];
        $source  = $parse['detected_lang'];
        $extra   = $parse['extra'];

        // Separate cache hits from strings that must go to the proxy
        $cache      = AITRFOEL_Cache_Manager::get_instance();
        $cached_map = [];
        $to_send    = [];
        foreach ($strings as $s) {
            $orig = $s['original'] ?? '';
            if (empty(trim($orig))) {
                continue;
            }
            $hit = $this->cache_enabled ? $cache->get_from_cache($orig, $target_lang, $source) : null;
            if ($hit) {
                $cached_map[$orig] = $hit;
            } else {
                $to_send[] = $orig;
            }
        }

        // Check word limit only against uncached strings — already-cached content is free.
        // This allows retrying a partially-translated page even when the budget is tight.
        if (!empty($to_send)) {
            $to_send_for_check = array_map(static function($s) { return ['original' => $s]; }, $to_send);
            $check = $this->check_word_limit($to_send_for_check, $page_id);
            if (is_wp_error($check)) {
                return $check;
            }
        }

        $context = [
            'page_id'      => $page_id,
            'target_lang'  => $target_lang,
            'source_lang'  => $source,
            'content_type' => $content_type,
            'strings'      => $strings,
            'extra'        => $extra,
            'cached_map'   => $cached_map,
            'to_send'      => $to_send,
            'start_time'   => microtime(true),
        ];

        if (empty($to_send)) {
            // All strings were already cached — apply immediately, no async round-trip needed
            $new_page_id = $this->apply_async_page($context, []);
            if (is_wp_error($new_page_id)) {
                return $new_page_id;
            }
            $key = 'aitrfoel_ah_' . $page_id . '_' . md5($target_lang);
            set_transient($key, $new_page_id, 10 * MINUTE_IN_SECONDS);
            return 'cache_hit:' . $key;
        }

        $api          = AITRFOEL_OpenAI_API::get_instance();
        $proxy_job_id = $api->submit_batch_async($to_send, $target_lang, $source);
        if (is_wp_error($proxy_job_id)) {
            return $proxy_job_id;
        }

        set_transient('aitrfoel_async_' . $proxy_job_id, $context, HOUR_IN_SECONDS);
        AITRFOEL_Logger::log(
            sprintf('Async page submit: page=%d lang=%s job=%s uncached=%d cached=%d',
                $page_id, $target_lang, $proxy_job_id, count($to_send), count($cached_map)),
            'info', $page_id
        );
        return $proxy_job_id;
    }

    /**
     * Async Phase 2: Check proxy job status; apply translations and create page when done.
     *
     * @param string $proxy_job_id Job ID returned by submit_page_async()
     * @return string|int|WP_Error
     *   'pending'  — job still running; call again in a few seconds
     *   int        — new_page_id on success
     *   WP_Error   — on failure
     */
    public function poll_page_async($proxy_job_id) {
        // Elementor async jobs use a multi-job manifest stored in the context transient;
        // route them to the dedicated poller before the single-job logic below.
        $maybe_ctx = get_transient('aitrfoel_async_' . $proxy_job_id);
        if (is_array($maybe_ctx)
            && ($maybe_ctx['content_type'] ?? '') === AITRFOEL_Content_Detector::TYPE_ELEMENTOR
            && !empty($maybe_ctx['jobs'])) {
            return $this->poll_elementor_async($proxy_job_id, $maybe_ctx);
        }

        $api    = AITRFOEL_OpenAI_API::get_instance();
        $status = $api->get_async_job_status($proxy_job_id);

        if (is_wp_error($status)) {
            return $status;
        }

        if (in_array($status['status'], ['pending', 'processing'], true)) {
            return 'pending';
        }

        if ($status['status'] === 'error') {
            return new WP_Error(
                'proxy_job_error',
                $status['error'] ?? __('Translation job failed on server.', 'ai-translator-for-elementor-and-polylang')
            );
        }

        // status === 'done'
        $context = get_transient('aitrfoel_async_' . $proxy_job_id);
        if (!$context) {
            return new WP_Error(
                'context_expired',
                __('Translation context expired. Please retry the translation.', 'ai-translator-for-elementor-and-polylang')
            );
        }

        // Proxy returns translations as an array ordered the same as $context['to_send']
        $proxy_translations = $status['translations'] ?? [];
        $full_map           = $context['cached_map'];
        foreach ($context['to_send'] as $i => $orig) {
            if (isset($proxy_translations[$i])) {
                $translated = method_exists($api, 'sanitize_chatty_response')
                    ? $api->sanitize_chatty_response($orig, $proxy_translations[$i])
                    : $proxy_translations[$i];
                $full_map[$orig] = $translated;
            }
        }

        $new_page_id = $this->apply_async_page($context, $full_map);

        if (!is_wp_error($new_page_id)) {
            delete_transient('aitrfoel_async_' . $proxy_job_id);
            // Cache freshly translated strings for future re-use
            if ($this->cache_enabled) {
                $cache = AITRFOEL_Cache_Manager::get_instance();
                foreach ($context['to_send'] as $i => $orig) {
                    if (!empty($full_map[$orig])) {
                        $cache->set_to_cache($orig, $full_map[$orig], $context['target_lang'], $context['source_lang'], 'gpt-4o-mini');
                    }
                }
            }
        }

        return $new_page_id;
    }

    /**
     * Elementor async Phase 1: parse the page, extract placeholders, chunk large
     * fields, and submit all prepared pieces to the proxy async endpoint as one or
     * more jobs (<=90 texts each, under the server's 100-text cap). A manifest mapping
     * every submitted text back to its Elementor path + chunk position is stored in a
     * context transient keyed by the first job id (the returned poll handle).
     *
     * @return string|WP_Error first job id (poll handle), or WP_Error to signal the
     *                         caller to fall back to the synchronous path.
     */
    private function submit_elementor_async($page_id, $target_lang, $source_lang = 'auto') {
        $raw = get_post_meta($page_id, '_elementor_data', true);
        if (empty($raw)) {
            return new WP_Error('no_elementor_data', __('No Elementor data found for this page.', 'ai-translator-for-elementor-and-polylang'));
        }
        $data = json_decode($raw, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
            return new WP_Error('json_decode_error', __('Failed to decode Elementor data.', 'ai-translator-for-elementor-and-polylang'));
        }

        $parser  = AITRFOEL_Widget_Parser::get_instance();
        $map     = $parser->parse_for_translation($data);
        $strings = array_values($map);
        if (empty($strings)) {
            return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
        }

        // Resolve a source language for caching/locale (the server still auto-detects).
        $detected_lang = $source_lang;
        if ($source_lang === 'auto') {
            $cached_lang   = get_transient('aitrfoel_detected_lang_' . $page_id . '_' . $target_lang);
            $detected_lang = $cached_lang ?: 'auto';
        }

        // Build ordered units: one prepared (placeholder-extracted) text piece per chunk.
        $max           = AITRFOEL_OpenAI_API::LARGE_TEXT_THRESHOLD;
        $units         = []; // [ ['path'=>, 'chunk_index'=>, 'n_chunks'=>, 'prepared'=>], ... ]
        $ph_maps       = []; // path => placeholder map
        $originals     = []; // path => original (for link restore)
        $prepared_full = []; // path => full prepared field (cache key, matches sync path)
        foreach ($strings as $row) {
            if (!is_array($row)) continue;
            $path = (string)($row['path'] ?? '');
            $orig = (string)($row['original'] ?? '');
            if ($path === '' || trim($orig) === '') continue;

            $prepared = $orig;
            $ph       = [];
            if (method_exists($parser, 'i18n_extract_with_placeholders')) {
                list($prepared, $ph) = $parser->i18n_extract_with_placeholders($orig);
            }
            $ph_maps[$path]       = is_array($ph) ? $ph : [];
            $originals[$path]     = $orig;
            $prepared_full[$path] = $prepared;

            $pieces = (strlen($prepared) > $max)
                ? AITRFOEL_OpenAI_API::split_html_into_chunks($prepared, $max)
                : [$prepared];
            if (empty($pieces)) { $pieces = [$prepared]; }

            $n = count($pieces);
            foreach ($pieces as $ci => $piece) {
                $units[] = ['path' => $path, 'chunk_index' => $ci, 'n_chunks' => $n, 'prepared' => $piece];
            }
        }

        if (empty($units)) {
            return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
        }

        $api = AITRFOEL_OpenAI_API::get_instance();

        // Submit in jobs of <=90 texts (server caps at 100). Global unit index == send index.
        $JOB_MAX = 90;
        $jobs    = []; // ['job_id'=>, 'offset'=>, 'count'=>]
        $total   = count($units);
        for ($offset = 0; $offset < $total; $offset += $JOB_MAX) {
            $slice  = array_slice($units, $offset, $JOB_MAX);
            $texts  = array_map(static function ($u) { return $u['prepared']; }, $slice);
            $job_id = $api->submit_batch_async($texts, $target_lang, $source_lang);
            if (is_wp_error($job_id)) {
                return $job_id; // caller falls back to sync
            }
            $jobs[] = ['job_id' => $job_id, 'offset' => $offset, 'count' => count($texts)];
        }

        $handle  = $jobs[0]['job_id'];
        $context = [
            'content_type'  => AITRFOEL_Content_Detector::TYPE_ELEMENTOR,
            'page_id'       => $page_id,
            'target_lang'   => $target_lang,
            'source_lang'   => $source_lang,
            'detected_lang' => $detected_lang,
            'units'         => $units,
            'ph_maps'       => $ph_maps,
            'originals'     => $originals,
            'prepared_full' => $prepared_full,
            'jobs'          => $jobs,
            'start_time'    => microtime(true),
        ];
        set_transient('aitrfoel_async_' . $handle, $context, HOUR_IN_SECONDS);

        AITRFOEL_Logger::log(
            sprintf('Elementor async submit: page=%d lang=%s handle=%s units=%d jobs=%d',
                $page_id, $target_lang, $handle, $total, count($jobs)),
            'info', $page_id
        );
        return $handle;
    }

    /**
     * Elementor async Phase 2: poll every job; once all are done, reassemble chunks
     * per path, restore placeholders, run the (bounded) gap-filler, apply to the
     * Elementor data, and save the translated page.
     *
     * @return string|int|WP_Error 'pending' | new_page_id | WP_Error
     */
    private function poll_elementor_async($handle, $context) {
        $api = AITRFOEL_OpenAI_API::get_instance();

        // Lay every job's translations into a global, send-ordered results array.
        $resolved = []; // global unit index => translated piece
        foreach ($context['jobs'] as $job) {
            $status = $api->get_async_job_status($job['job_id']);
            if (is_wp_error($status)) {
                return $status;
            }
            $st = $status['status'] ?? 'pending';
            if (in_array($st, ['pending', 'processing'], true)) {
                return 'pending';
            }
            if ($st === 'error') {
                return new WP_Error('proxy_job_error', $status['error'] ?? __('Translation job failed on server.', 'ai-translator-for-elementor-and-polylang'));
            }
            $job_translations = $status['translations'] ?? [];
            $offset = (int) $job['offset'];
            foreach ($job_translations as $k => $tr) {
                $resolved[$offset + $k] = $tr;
            }
        }

        $parser        = AITRFOEL_Widget_Parser::get_instance();
        $units         = $context['units'];
        $ph_maps       = $context['ph_maps'];
        $originals     = $context['originals'];
        $prepared_full = $context['prepared_full'] ?? [];
        $target_lang   = $context['target_lang'];
        $detected_lang = $context['detected_lang'];

        // Reassemble translated pieces per path (ordered by chunk_index via unit order).
        $assembled = []; // path => reassembled pre-restore translated text
        foreach ($units as $i => $u) {
            $path     = $u['path'];
            $piece_tr = isset($resolved[$i]) ? (string) $resolved[$i] : $u['prepared']; // fallback: keep source piece
            $assembled[$path] = ($assembled[$path] ?? '') . $piece_tr;
        }

        // Restore placeholders + cleanup per path → final translation map (by path).
        $translations = [];
        foreach ($assembled as $path => $tr_prepared) {
            $final = $tr_prepared;
            $pmap  = $ph_maps[$path] ?? [];
            if (!empty($pmap) && method_exists($parser, 'i18n_restore_placeholders')) {
                $final = $parser->i18n_restore_placeholders($final, $pmap);
            }
            if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                $final = $parser->i18n_cleanup_artifacts($final);
            }
            if (method_exists($parser, 'restore_lost_links') && isset($originals[$path])) {
                $final = $parser->restore_lost_links($final, $originals[$path]);
            }
            $translations[$path] = $final;
        }

        // Gap-filler second pass (now bounded/fast) to fix missed/under-translated strings.
        if (class_exists('AITRFOEL_Gap_Filler')) {
            $gf_strings = [];
            foreach ($originals as $path => $orig) {
                $gf_strings[] = ['path' => $path, 'original' => $orig, 'type' => 'field'];
            }
            $opts = [
                'levenshtein_threshold' => (int) get_option('aitrfoel_gap_lev_threshold', 3),
                'residue_patterns'      => (string) get_option('aitrfoel_gap_residue_patterns', '/\\b(Read more|Číst dál|Читать далее)\\b/ui'),
                'max_retry'             => (int) get_option('aitrfoel_gap_max_retry', 1),
                'enable_postprocessor'  => (bool) get_option('aitrfoel_gap_enable_postprocessor', true),
                'page_id'               => $context['page_id'],
            ];
            AITRFOEL_Gap_Filler::run($gf_strings, $translations, $target_lang, $detected_lang, $opts);
        }

        // Apply to a fresh copy of the Elementor data and save.
        $raw  = get_post_meta($context['page_id'], '_elementor_data', true);
        $data = json_decode($raw, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
            return new WP_Error('json_decode_error', __('Failed to decode Elementor data.', 'ai-translator-for-elementor-and-polylang'));
        }
        $translated_data = $parser->apply_translations($data, $translations);

        // Deep cleanup of any escaped tags / orphan placeholders left in string fields.
        if (method_exists($parser, 'i18n_cleanup_artifacts')) {
            $walker = function (&$node) use (&$walker, $parser) {
                if (is_string($node)) {
                    $node = $parser->i18n_cleanup_artifacts($node);
                    return;
                }
                if (is_array($node)) { foreach ($node as &$v) { $walker($v); } unset($v); }
            };
            $walker($translated_data);
        }

        if (!is_array($translated_data) ||
            (method_exists($parser, 'validate_structure') && !$parser->validate_structure($translated_data))) {
            return new WP_Error('invalid_structure', __('Translation resulted in invalid Elementor structure.', 'ai-translator-for-elementor-and-polylang'));
        }

        // Same post-apply transforms as the sync path: link localization + menu/template swaps.
        try { $this->localize_internal_links($translated_data, $target_lang); } catch (Exception $e) { /* non-fatal */ }
        $menu_id_map = get_option('aitrfoel_menu_id_map', []);
        if (!empty($menu_id_map)) {
            $this->swap_menu_ids_in_elementor_data($translated_data, $menu_id_map, $target_lang);
        }
        if (method_exists($this, 'swap_elementor_template_refs_in_data')) {
            $this->swap_elementor_template_refs_in_data($translated_data, $target_lang);
        }

        // Translate the post title (single quick call).
        $orig_post = get_post($context['page_id']);
        $pre_title = null;
        if ($orig_post && !empty($orig_post->post_title)) {
            $t = $api->translate_title($orig_post->post_title, $target_lang, $context['source_lang']);
            if (!is_wp_error($t) && !empty($t)) { $pre_title = $t; }
        }

        $new_page_id = $this->create_translated_page($context['page_id'], $translated_data, $target_lang, $detected_lang, $pre_title, false);
        if (is_wp_error($new_page_id)) {
            return $new_page_id;
        }

        // Persist cache for future reuse (full prepared field => reassembled pre-restore
        // translated), matching the key/value scheme the sync large-text path uses.
        if ($this->cache_enabled) {
            $cache = AITRFOEL_Cache_Manager::get_instance();
            $model = get_option('aitrfoel_openai_model', 'gpt-4o-mini');
            foreach ($prepared_full as $path => $src_prep) {
                if (!empty($assembled[$path]) && !empty($src_prep)) {
                    $cache->set_to_cache($src_prep, $assembled[$path], $target_lang, $detected_lang, $model);
                }
            }
        }

        if (class_exists('AITRFOEL_Translation_History')) {
            AITRFOEL_Translation_History::record([
                'original_post_id'   => $context['page_id'],
                'translated_post_id' => $new_page_id,
                'source_lang'        => $detected_lang,
                'target_lang'        => $target_lang,
                'content_type'       => 'elementor',
                'word_count'         => 0,
                'status'             => 'success',
                'duration'           => round(microtime(true) - ($context['start_time'] ?? microtime(true)), 2),
            ]);
        }

        delete_transient('aitrfoel_async_' . $handle);
        AITRFOEL_License_Manager_Server::get_instance()->clear_status_cache();

        AITRFOEL_Logger::log(
            sprintf('Elementor async complete: page=%d → new_page=%d lang=%s', $context['page_id'], $new_page_id, $target_lang),
            'success', $context['page_id'], $new_page_id
        );
        return $new_page_id;
    }

    /**
     * Parse a non-Elementor page into a strings array for async submission.
     * Handles Classic Editor, Gutenberg, and WooCommerce content types.
     *
     * @param int    $page_id      Page ID
     * @param string $content_type One of AITRFOEL_Content_Detector::TYPE_* constants
     * @param string $source_lang  Source language code or 'auto'
     * @return array|WP_Error {strings, detected_lang, extra}
     */
    private function parse_page_content_for_async($page_id, $content_type, $source_lang) {
        switch ($content_type) {
            case AITRFOEL_Content_Detector::TYPE_CLASSIC: {
                $post = get_post($page_id);
                if (!$post || empty($post->post_content)) {
                    return new WP_Error('no_content', __('No content found in this page.', 'ai-translator-for-elementor-and-polylang'));
                }
                $parser  = AITRFOEL_Classic_Parser::get_instance();
                $map     = $parser->parse_for_translation($page_id);
                if (empty($map)) {
                    return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
                }
                $strings          = array_values($map);
                $title_in_content = false;
                foreach ($strings as $s) {
                    if (trim(wp_strip_all_tags($s['original'] ?? '')) === trim($post->post_title)) {
                        $title_in_content = true;
                        break;
                    }
                }
                if (!empty($post->post_title) && !$title_in_content) {
                    $strings[] = ['original' => $post->post_title, 'path' => '__aitrfoel_post_title__', 'type' => 'title', 'widget_type' => 'classic_editor'];
                }
                $detected = $this->detect_source_language_from_content($strings, $source_lang, $page_id);
                return [
                    'strings'      => $strings,
                    'detected_lang'=> $detected,
                    'extra'        => [
                        'post_title'       => $post->post_title,
                        'post_content'     => $post->post_content,
                        'title_in_content' => $title_in_content,
                    ],
                ];
            }

            case AITRFOEL_Content_Detector::TYPE_GUTENBERG: {
                $post = get_post($page_id);
                if (!$post || empty($post->post_content)) {
                    return new WP_Error('no_content', __('No content found in this page.', 'ai-translator-for-elementor-and-polylang'));
                }
                $parser  = AITRFOEL_Gutenberg_Parser::get_instance();
                $map     = $parser->parse_for_translation($page_id);
                if (empty($map)) {
                    return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
                }
                $strings          = array_values($map);
                $title_in_content = false;
                foreach ($strings as $s) {
                    if (trim(wp_strip_all_tags($s['original'] ?? '')) === trim($post->post_title)) {
                        $title_in_content = true;
                        break;
                    }
                }
                if (!empty($post->post_title) && !$title_in_content) {
                    $strings[] = ['original' => $post->post_title, 'path' => '__aitrfoel_post_title__', 'type' => 'title', 'widget_type' => 'gutenberg'];
                }
                $detected = $this->detect_source_language_from_content($strings, $source_lang, $page_id);
                return [
                    'strings'      => $strings,
                    'detected_lang'=> $detected,
                    'extra'        => [
                        'post_title'       => $post->post_title,
                        'post_content'     => $post->post_content,
                        'title_in_content' => $title_in_content,
                    ],
                ];
            }

            case AITRFOEL_Content_Detector::TYPE_WOOCOMMERCE: {
                $post = get_post($page_id);
                if (!$post) {
                    return new WP_Error('no_content', __('Product not found.', 'ai-translator-for-elementor-and-polylang'));
                }
                $texts     = [];
                $text_keys = [];
                if (!empty($post->post_title))         { $texts[] = $post->post_title;   $text_keys[] = 'title'; }
                if (!empty(trim($post->post_excerpt))) { $texts[] = $post->post_excerpt; $text_keys[] = 'excerpt'; }
                if (!empty(trim($post->post_content))) { $texts[] = $post->post_content; $text_keys[] = 'content'; }
                if (empty($texts)) {
                    return new WP_Error('no_translatable_content', __('This product has no translatable content.', 'ai-translator-for-elementor-and-polylang'));
                }
                $strings = [];
                foreach ($texts as $i => $t) {
                    $strings[] = ['original' => $t, 'path' => $text_keys[$i], 'type' => 'text', 'widget_type' => 'woocommerce_product'];
                }
                $detected = $this->detect_source_language_from_content($strings, $source_lang, $page_id);
                return [
                    'strings'      => $strings,
                    'detected_lang'=> $detected,
                    'extra'        => ['texts' => $texts, 'text_keys' => $text_keys],
                ];
            }

            default:
                return new WP_Error('unsupported_content', __('Content type does not support async translation.', 'ai-translator-for-elementor-and-polylang'));
        }
    }

    /**
     * Apply a complete original→translated map to a page and create the translated copy.
     * Called by poll_page_async() once all translations are available (and for the all-cached case).
     *
     * @param array $context       Context array stored by submit_page_async()
     * @param array $translations_map Additional original→translated pairs (from proxy result)
     * @return int|WP_Error new_page_id or WP_Error
     */
    private function apply_async_page($context, $translations_map) {
        $page_id      = $context['page_id'];
        $target_lang  = $context['target_lang'];
        $source_lang  = $context['source_lang'];
        $content_type = $context['content_type'];
        $extra        = $context['extra'];
        $start_time   = $context['start_time'] ?? microtime(true);
        $strings      = $context['strings'];

        // Merge cached hits + proxy results into one complete map
        $full_map = array_merge($context['cached_map'] ?? [], $translations_map);

        switch ($content_type) {
            case AITRFOEL_Content_Detector::TYPE_CLASSIC: {
                $parser             = AITRFOEL_Classic_Parser::get_instance();
                $translated_content = $parser->apply_translations($extra['post_content'], $full_map);
                $translated_title   = null;
                if (!$extra['title_in_content'] && isset($full_map[$extra['post_title']])) {
                    $translated_title = $full_map[$extra['post_title']];
                } elseif ($extra['title_in_content']) {
                    foreach ($strings as $s) {
                        if (trim(wp_strip_all_tags($s['original'] ?? '')) === trim($extra['post_title'])
                            && isset($full_map[$s['original']])) {
                            $translated_title = wp_strip_all_tags($full_map[$s['original']]);
                            break;
                        }
                    }
                }
                $new_page_id = $this->create_translated_page_generic($page_id, $translated_content, $target_lang, $source_lang, 'classic', $translated_title);
                if (!is_wp_error($new_page_id)) {
                    $this->track_translation_success($page_id, $new_page_id, $target_lang, $source_lang, $start_time, 'classic');
                }
                return $new_page_id;
            }

            case AITRFOEL_Content_Detector::TYPE_GUTENBERG: {
                $parser             = AITRFOEL_Gutenberg_Parser::get_instance();
                $translated_content = $parser->apply_translations($extra['post_content'], $full_map);
                $translated_title   = null;
                if (!$extra['title_in_content'] && isset($full_map[$extra['post_title']])) {
                    $translated_title = $full_map[$extra['post_title']];
                    unset($full_map[$extra['post_title']]);
                } elseif ($extra['title_in_content']) {
                    foreach ($strings as $s) {
                        if (trim(wp_strip_all_tags($s['original'] ?? '')) === trim($extra['post_title'])
                            && isset($full_map[$s['original']])) {
                            $translated_title = wp_strip_all_tags($full_map[$s['original']]);
                            break;
                        }
                    }
                }
                $new_page_id = $this->create_translated_page_generic($page_id, $translated_content, $target_lang, $source_lang, 'gutenberg', $translated_title);
                if (!is_wp_error($new_page_id)) {
                    $this->track_translation_success($page_id, $new_page_id, $target_lang, $source_lang, $start_time, 'gutenberg');
                }
                return $new_page_id;
            }

            case AITRFOEL_Content_Detector::TYPE_WOOCOMMERCE: {
                $texts              = $extra['texts'];
                $text_keys          = $extra['text_keys'];
                $translated_title   = null;
                $translated_excerpt = null;
                $translated_content = null;
                foreach ($texts as $i => $orig) {
                    $tr = $full_map[$orig] ?? null;
                    if (!$tr) continue;
                    if ($text_keys[$i] === 'title')   { $translated_title   = $tr; }
                    if ($text_keys[$i] === 'excerpt') { $translated_excerpt = $tr; }
                    if ($text_keys[$i] === 'content') { $translated_content = $tr; }
                }
                if (!$translated_title) {
                    $post = get_post($page_id);
                    $translated_title = ($post->post_title ?? '') . ' (' . strtoupper($target_lang) . ')';
                }
                $new_page_id = $this->create_translated_page_generic($page_id, $translated_content ?: '', $target_lang, $source_lang, 'woocommerce', $translated_title);
                if (!is_wp_error($new_page_id)) {
                    if ($translated_excerpt) {
                        wp_update_post(['ID' => $new_page_id, 'post_excerpt' => $translated_excerpt]);
                    }
                    $this->track_translation_success($page_id, $new_page_id, $target_lang, $source_lang, $start_time, 'woocommerce');
                }
                return $new_page_id;
            }

            default:
                return new WP_Error('unsupported_content', __('Cannot apply translations: unsupported content type.', 'ai-translator-for-elementor-and-polylang'));
        }
    }

    /**
     * Translate Elementor page (original functionality preserved)
     * 
     * @param int $page_id Original page ID
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param array $strings_to_translate Selected strings for translation (optional)
     * @param float $translation_start_time Start time for analytics
     * @return int|WP_Error New page ID or error
     */
    private function translate_elementor_page($page_id, $target_lang, $source_lang = 'auto', $strings_to_translate = [], $translation_start_time = null) {
        if (!$translation_start_time) {
            $translation_start_time = microtime(true);
        }
        
        // Get Elementor data
        $elementor_data = get_post_meta($page_id, '_elementor_data', true);
        
        if (empty($elementor_data)) {
            $this->track_translation_failure('no_elementor_data', 'No Elementor data found', $translation_start_time);
            return new WP_Error('no_elementor_data', __('No Elementor data found for this page.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        $elementor_data_array = json_decode($elementor_data, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            $this->track_translation_failure('json_decode_error', 'Failed to decode Elementor data', $translation_start_time);
            return new WP_Error('json_decode_error', __('Failed to decode Elementor data.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // If no specific strings provided, parse all translatable content
        if (empty($strings_to_translate)) {
            $parser = AITRFOEL_Widget_Parser::get_instance();
            $translation_map = $parser->parse_for_translation($elementor_data_array);
            $strings_to_translate = array_values($translation_map);
        }
        
        // ==========================================
        // WORD LIMIT & COOLDOWN ENFORCEMENT
        // ==========================================
        
        // Get current license status and usage
        $license_manager = AITRFOEL_License_Manager_Server::get_instance();
        $license_status = $license_manager->get_license_status();
        
        $plan = $license_status['plan'];
        $word_limit = $license_status['word_limit'];
        $words_used = $license_status['words_used'];
        $words_left = $license_status['words_left'];
        
        // Calculate estimated word count for this translation
        // Server counts both source + translated words, so apply 2× factor
        $source_words = 0;
        $debug_word_breakdown = []; // DEBUG: track per-string word counts
        foreach ($strings_to_translate as $string_data) {
            if (isset($string_data['original'])) {
                $text = wp_strip_all_tags($string_data['original']);
                $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                $wc = self::count_words($text);
                $source_words += $wc;
                $debug_word_breakdown[] = $wc . ':' . mb_substr($text, 0, 60);
            }
        }
        // DEBUG: Log per-string word counts
        AITRFOEL_Logger::log(
            sprintf('[WORD-DEBUG] Total strings: %d, Source words: %d, Breakdown: %s',
                count($strings_to_translate), $source_words, implode(' | ', $debug_word_breakdown)),
            'info',
            $page_id
        );
        $server_factor = defined('AITRFOEL_Translate_All::SERVER_USAGE_FACTOR') ? AITRFOEL_Translate_All::SERVER_USAGE_FACTOR : 2;

        // Refine the estimate by subtracting words for strings already in cache.
        // On retries, most strings are cached so the real cost is a small fraction
        // of the full page — without this, retries are blocked unnecessarily.
        $prelim_source_lang = $source_lang;
        if ($source_lang === 'auto') {
            $cached_lang_transient = get_transient('aitrfoel_detected_lang_' . $page_id . '_' . $target_lang);
            if ($cached_lang_transient) {
                $prelim_source_lang = $cached_lang_transient;
            }
        }
        AITRFOEL_Logger::log(
            sprintf('[WORD-PRECHECK] source_lang=%s, prelim_source_lang=%s, cache_enabled=%s',
                $source_lang, $prelim_source_lang, $this->cache_enabled ? 'yes' : 'no'),
            'debug', $page_id
        );
        $cache_precheck       = AITRFOEL_Cache_Manager::get_instance();
        $uncached_source_words = 0;
        foreach ($strings_to_translate as $string_data) {
            if (!isset($string_data['original'])) continue;
            $raw = (string) $string_data['original'];
            $stripped = html_entity_decode(wp_strip_all_tags($raw), ENT_QUOTES | ENT_HTML5, 'UTF-8');
            $wc = self::count_words($stripped);
            if ($wc === 0) continue;

            if (strlen($raw) > AITRFOEL_OpenAI_API::LARGE_TEXT_THRESHOLD) {
                // Large string: check regular cache first (set when translation completes fully).
                if ($this->cache_enabled) {
                    $prepared_large = $raw;
                    if (method_exists($parser, 'i18n_extract_with_placeholders')) {
                        list($prepared_large,) = $parser->i18n_extract_with_placeholders($raw);
                    }
                    $large_cached = $cache_precheck->get_from_cache($prepared_large, $target_lang, $prelim_source_lang);
                    AITRFOEL_Logger::log(
                        sprintf('[WORD-PRECHECK] Large string (%d chars, %d words): regular cache %s (lang=%s→%s)',
                            strlen($raw), $wc, $large_cached !== null ? 'HIT' : 'MISS', $prelim_source_lang, $target_lang),
                        'debug', $page_id
                    );
                    if ($large_cached !== null) {
                        continue; // fully translated and in regular cache
                    }
                }
                // Not in regular cache: check per-chunk transients to gauge remaining cost.
                $total_approx    = (int) ceil(strlen($raw) / AITRFOEL_OpenAI_API::LARGE_TEXT_THRESHOLD);
                $chunk_key_base  = 'aitrfoel_chunk_' . md5($raw . $target_lang);
                $cached_chunks   = 0;
                for ($ci = 0; $ci < $total_approx; $ci++) {
                    if (get_transient($chunk_key_base . '_' . $ci) !== false) {
                        $cached_chunks++;
                    }
                }
                $uncached_ratio = $total_approx > 0
                    ? max(0.0, ($total_approx - $cached_chunks) / $total_approx)
                    : 1.0;
                AITRFOEL_Logger::log(
                    sprintf('[WORD-PRECHECK] Large string chunk transients: %d/%d cached, ratio_uncached=%.2f',
                        $cached_chunks, $total_approx, $uncached_ratio),
                    'debug', $page_id
                );
                $uncached_source_words += (int) ceil($wc * $uncached_ratio);
                continue;
            }

            // Regular string: check translation cache.
            if ($this->cache_enabled) {
                $prepared = $raw;
                if (method_exists($parser, 'i18n_extract_with_placeholders')) {
                    list($prepared,) = $parser->i18n_extract_with_placeholders($raw);
                }
                if ($cache_precheck->get_from_cache($prepared, $target_lang, $prelim_source_lang) !== null) {
                    continue; // already translated and cached — no words needed
                }
            }
            $uncached_source_words += $wc;
        }

        $estimated_words = $uncached_source_words * $server_factor;

        AITRFOEL_Logger::log(
            sprintf('Word limit check: Plan=%s, Limit=%d, Used=%d, Left=%d, TotalSourceWords=%d, UncachedWords=%d, EstimatedUsage=%d (×%d)',
                $plan, $word_limit, $words_used, $words_left, $source_words, $uncached_source_words, $estimated_words, $server_factor),
            'info',
            $page_id
        );
        
        // Check if user has enough words remaining
        if ($word_limit !== -1 && $estimated_words > $words_left) {
            // Check if user is on cooldown (Free plan only)
            if (strtolower($plan) === 'free') {
                $cooldown_end = get_option('aitrfoel_cooldown_end_date', 0);
                
                if ($cooldown_end > time()) {
                    // Still in cooldown period
                    $days_left = ceil(($cooldown_end - time()) / DAY_IN_SECONDS);
                    
                    AITRFOEL_Logger::log(
                        sprintf('Translation blocked: Free plan in cooldown for %d more days', $days_left),
                        'warning',
                        $page_id
                    );
                    
                    return new WP_Error(
                        'cooldown_active',
                        sprintf(
                            /* translators: 1: Word limit (formatted number), 2: Number of days remaining */
                            __('You have reached your monthly limit of %1$s words. Free plan users must wait %2$d more days for the limit to reset. Upgrade to a paid plan for immediate access.', 'ai-translator-for-elementor-and-polylang'),
                            number_format($word_limit),
                            $days_left
                        )
                    );
                }
            }
            
            // Not enough words for this translation
            AITRFOEL_Logger::log(
                sprintf('Translation blocked: Insufficient words (need %d, have %d)', $estimated_words, $words_left),
                'warning',
                $page_id
            );
            
            $upgrade_link = admin_url('admin.php?page=ai-translator-for-elementor-and-polylang-pricing');
            
            return new WP_Error(
                'insufficient_words',
                sprintf(
                    /* translators: 1: Estimated words needed (formatted number), 2: Words remaining (formatted number), 3: Upgrade URL */
                    __('This translation requires approximately %1$s words, but you only have %2$s words remaining in your plan. Please <a href="%3$s">upgrade your plan</a> or wait for your monthly limit to reset.', 'ai-translator-for-elementor-and-polylang'),
                    number_format($estimated_words),
                    number_format($words_left),
                    esc_url($upgrade_link)
                )
            );
        }
        
        // ==========================================
        // END WORD LIMIT CHECK
        // ==========================================
        // Language detection if auto
        AITRFOEL_Logger::log("Source language parameter: '$source_lang'", 'debug', $page_id);
        $detected_lang = $source_lang;
        if ($source_lang === 'auto') {
            AITRFOEL_Logger::log('Starting language auto-detection', 'debug', $page_id);
            
            // Log sample of text for debugging
            if (!empty($strings_to_translate)) {
                $sample_text = isset($strings_to_translate[0]['original']) ? $strings_to_translate[0]['original'] : '';
                $sample_display = mb_substr($sample_text, 0, 100) . '...';
                AITRFOEL_Logger::log("Sample text for detection: '$sample_display'", 'debug', $page_id);
            }
            
            $detected_lang = $this->detect_source_language($strings_to_translate, $page_id);
            if (!$detected_lang) {
                AITRFOEL_Logger::log('Auto-detection failed, using fallback: en', 'warning', $page_id);
                $detected_lang = 'en'; // sensible fallback
            } else {
                AITRFOEL_Logger::log("Language detected: '$detected_lang'", 'info', $page_id);
            }
    }

        // Persist the resolved language code so AJAX retries can send an explicit code
        // instead of 'auto'. This keeps the cache key (hash + target_lang + source_lang)
        // identical across all partial-save passes, preventing full retranslation.
        set_transient('aitrfoel_detected_lang_' . $page_id . '_' . $target_lang, $detected_lang, 15 * MINUTE_IN_SECONDS);

        // Enhanced logging for debugging
        AITRFOEL_Logger::log(
            sprintf('Translation summary: Page %d, %s → %s, %d strings to translate', 
                $page_id, $detected_lang, $target_lang, count($strings_to_translate)),
            'info',
            $page_id
        );
        
        // Log translation start
        AITRFOEL_Logger::log(
            sprintf('Starting translation of page %d to %s', $page_id, $target_lang),
            'info',
            $page_id
        );
        
        // Initialize services based on selected provider (OpenAI only)
        $api_provider = 'openai'; // Simplified to OpenAI only
        AITRFOEL_Logger::log("Using API provider: '$api_provider'", 'debug', $page_id);
        
        $api = AITRFOEL_OpenAI_API::get_instance();
        if (method_exists($api, 'reset_usage_session')) {
            $api->reset_usage_session();
        }
        
        $cache = AITRFOEL_Cache_Manager::get_instance();
        $parser = AITRFOEL_Widget_Parser::get_instance();
        
        // Include post title in batch to avoid a separate API call
        $original_post = get_post($page_id);
        $title_batch_key = '__aitrfoel_post_title__';
        $pre_translated_title = null;
        $title_already_in_batch = false;
        if ($original_post && !empty($original_post->post_title)) {
            // Check if title already exists in parsed content
            foreach ($strings_to_translate as $s) {
                $stripped = trim(wp_strip_all_tags($s['original'] ?? ''));
                if ($stripped === trim($original_post->post_title)) {
                    $title_already_in_batch = true;
                    break;
                }
            }
            if (!$title_already_in_batch) {
                $strings_to_translate[] = [
                    'original' => $original_post->post_title,
                    'path' => $title_batch_key,
                    'type' => 'title',
                    'widget_type' => 'elementor',
                ];
            }
        }
        
        // Process translations in batches
        $batch_start_time = microtime(true);
        AITRFOEL_Logger::log(sprintf('[TIMING] Starting batch processing: %d strings', count($strings_to_translate)), 'info', $page_id);
        $translations = [];
        $total_strings = count($strings_to_translate);
        $processed = 0;
        
        // Group strings by cache status - uncached strings get batched
        $cached_translations = [];
        $uncached_strings = [];
        $uncached_paths = [];
        
        $uncached_items = [];
        foreach ($strings_to_translate as $string_data) {
            // Validate string data structure
            if (!is_array($string_data) || !isset($string_data['original'])) {
                AITRFOEL_Logger::log('Invalid string data structure, skipping', 'warning', $page_id);
                continue;
            }
            
            $raw_original = (string)$string_data['original'];
            $path = $string_data['path'] ?? '';

            // Extract inline placeholders using parser helper
            $ph_map = [];
            $prepared_text = $raw_original;
            if (method_exists($parser, 'i18n_extract_with_placeholders')) {
                list($prepared_text, $ph_map) = $parser->i18n_extract_with_placeholders($raw_original);
                // Diagnostics: log placeholder count per string
                if (is_array($ph_map) && !empty($ph_map)) {
                    AITRFOEL_Logger::log(sprintf('Placeholders extracted: %d for path %s', count($ph_map), (string)($path ?? 'n/a')), 'debug', $page_id);
                }
            }
            
            // Normalize to NFC if possible
            if (class_exists('Normalizer') && is_string($prepared_text)) {
                $prepared_text = Normalizer::normalize($prepared_text, Normalizer::FORM_C);
            }
            
            // Skip empty or invalid text
            if (empty(trim((string)$prepared_text))) {
                continue;
            }
            
            // Check cache first (only if enabled)
            $cached = null;
            if ($this->cache_enabled) {
                $cached = $cache->get_from_cache($prepared_text, $target_lang, $detected_lang);
                // Validate cached entry: reject if translation is too similar to source
                // This prevents using stale cache from earlier failed translations
                if ($cached !== null) {
                    $cached_clean = strtolower(trim(html_entity_decode(wp_strip_all_tags($cached), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                    $source_clean = strtolower(trim(html_entity_decode(wp_strip_all_tags($prepared_text), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                    if ($cached_clean === $source_clean) {
                        $cached = null; // Reject: cached translation is identical to source
                        AITRFOEL_Logger::log(sprintf('Cache rejected (identical to source): "%s"', mb_substr($prepared_text, 0, 60)), 'debug', $page_id);
                    }
                }
            }
                if ($cached) {
                // Restore placeholders before applying
                if (!empty($ph_map) && method_exists($parser, 'i18n_restore_placeholders')) {
                    $restored = $parser->i18n_restore_placeholders($cached, $ph_map);
                    if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                        $restored = $parser->i18n_cleanup_artifacts($restored);
                    }
                    // Try to restore lost links
                    if (method_exists($parser, 'restore_lost_links')) {
                        $restored = $parser->restore_lost_links($restored, $raw_original);
                    }
                    $cached_translations[$path] = $restored;
                } else {
                    $tmp = $this->restore_html_formatting($cached, $raw_original);
                    if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                        $tmp = $parser->i18n_cleanup_artifacts($tmp);
                    }
                    // Try to restore lost links
                    if (method_exists($parser, 'restore_lost_links')) {
                        $tmp = $parser->restore_lost_links($tmp, $raw_original);
                    }
                    $cached_translations[$path] = $tmp;
                }
                $processed++;
            } else {
                // Queue item for batch translation
                $uncached_items[] = [
                    'src' => $prepared_text,
                    'path' => $path,
                    'original' => $raw_original,
                    'ph_map' => $ph_map,
                ];
            }
        }

        // Self-imposed time budget per AJAX call.  If the host has disabled set_time_limit,
        // PHP can run for 20+ minutes on a huge page and be killed with nothing saved.
        // We cap each call at 200 s, save whatever was translated, and return 'partial' so
        // the caller retries — the retry is fast because all strings are now in cache.
        $budget_seconds = (int) apply_filters('aitrfoel_translate_time_budget', 200);
        $budget_start   = microtime(true);
        $timed_out      = false;
        $word_limit_hit = false;

        // Propagate time budget to the API class so translate_large_text() can
        // honour the same deadline and cache completed chunks for fast retries.
        AITRFOEL_OpenAI_API::set_time_budget($budget_start, $budget_seconds);

        // Process uncached strings in batches
        if (!empty($uncached_items)) {
            $cache_elapsed = round((microtime(true) - $batch_start_time) * 1000);

            // Separate large strings from normal strings. Large strings are sent
            // directly to translate_text() (which chunks them) so they never block
            // the batch API and waste the time budget on a 60-second cURL timeout.
            $large_items  = [];
            $normal_items = [];
            foreach ($uncached_items as $item) {
                if (strlen($item['src']) > AITRFOEL_OpenAI_API::LARGE_TEXT_THRESHOLD) {
                    $large_items[] = $item;
                } else {
                    $normal_items[] = $item;
                }
            }
            AITRFOEL_Logger::log(sprintf('[TIMING] Cache check done in %dms: %d cached, %d uncached (%d large, %d normal)', $cache_elapsed, count($cached_translations), count($uncached_items), count($large_items), count($normal_items)), 'info', $page_id);
            $batches = array_chunk($normal_items, $this->batch_size);
            
            foreach ($batches as $batch_index => $batch_items) {
                // Group key is now just "batch" since we're not grouping by widget
                $gkey = 'batch';
                    $batch_texts = array_map(function($it){ return $it['src']; }, $batch_items);
                    $start = microtime(true);
                    AITRFOEL_Logger::log(sprintf('[EAT] page=%d lang=%s group="%s" batch=%d size=%d', $page_id, $target_lang, (string)$gkey, $batch_index, count($batch_texts)), 'info', $page_id);
                    // Use batch translation API (expects placeholderized texts)
                    $batch_results = $api->translate_batch($batch_texts, $target_lang, $detected_lang);
                    $elapsed_ms = (int)round((microtime(true) - $start) * 1000);
                    if (!is_wp_error($batch_results)) {
                        AITRFOEL_Logger::log(sprintf('[EAT] page=%d lang=%s group="%s" batch=%d results=%d time_ms=%d', $page_id, $target_lang, (string)$gkey, $batch_index, count((array)$batch_results), $elapsed_ms), 'info', $page_id);
                    }
                
                if (is_wp_error($batch_results)) {
                    $batch_error_msg = $batch_results->get_error_message();
                    AITRFOEL_Logger::log(
                        'Batch translation failed, falling back to individual: ' . $batch_error_msg,
                        'warning',
                        $page_id
                    );
                    
                    // If batch failed due to word limit, abort immediately — don't try individual calls
                    if (
                        strpos($batch_error_msg, 'Word limit') !== false ||
                        strpos($batch_error_msg, 'word limit') !== false ||
                        strpos($batch_error_msg, 'words remaining') !== false
                    ) {
                        return new WP_Error('word_limit_exceeded', $batch_error_msg);
                    }
                    
                    // Fallback to individual translation
                    $consecutive_limit_errors = 0;
                    foreach ($batch_items as $item) {
                        // Budget check inside fallback — each individual call can take up to 60 s;
                        // without this guard 20 calls × 60 s = 1200 s blocks the time-budget save.
                        if ((microtime(true) - $budget_start) >= $budget_seconds) {
                            $timed_out = true;
                            AITRFOEL_Logger::log(
                                sprintf('[TIMING] Time budget of %ds reached inside individual fallback (batch %d). Saving partial page.',
                                    $budget_seconds, $batch_index),
                                'warning',
                                $page_id
                            );
                            break 2;
                        }
                        $text = $item['src'];
                        $translated = $api->translate_text($text, $target_lang, $detected_lang);
                        
                        // CRITICAL DEBUG: Log what OpenAI returns for individual translations
                        AITRFOEL_Logger::log("Individual Translation Debug - Original text (first 200 chars): " . substr($text, 0, 200), 'debug');
                        AITRFOEL_Logger::log("Individual Translation Debug - Translated text (first 200 chars): " . substr(is_wp_error($translated) ? 'ERROR' : $translated, 0, 200), 'debug');
                        AITRFOEL_Logger::log("Individual Translation Debug - Original contains placeholders: " . (strpos($text, '≪') !== false ? 'YES' : 'NO'), 'debug');
                        AITRFOEL_Logger::log("Individual Translation Debug - Translated contains placeholders: " . (!is_wp_error($translated) && strpos($translated, '≪') !== false ? 'YES' : 'NO'), 'debug');
                        
                        if (!is_wp_error($translated)) {
                            $path = $item['path'];
                            $original = $item['original'];
                            $ph_map = $item['ph_map'];
                            
                            // Cache the translation (only if enabled)
                            if ($this->cache_enabled) {
                                $cache->set_to_cache(
                                    $text, // cache keyed by placeholderized source
                                    $translated, // store raw model output
                                    $target_lang,
                                    $detected_lang,
                                    get_option('aitrfoel_openai_model', 'gpt-3.5-turbo')
                                );
                            }
                            
                            // Residue retry: check for leftover phrases
                            $needs_retry = false;
                            foreach (['Read more','Číst dál','Читать далее'] as $marker) {
                                if (is_string($translated) && mb_stripos($translated, $marker, 0, 'UTF-8') !== false) {
                                    $needs_retry = true; break;
                                }
                            }
                            if ($needs_retry) {
                                $retry = $api->translate_text($text, $target_lang, $detected_lang);
                                if (!is_wp_error($retry) && is_string($retry) && $retry !== '') {
                                    $translated = $retry;
                                }
                            }
                            // Restore placeholders
                            if (!empty($ph_map) && method_exists($parser, 'i18n_restore_placeholders')) {
                                $final = $parser->i18n_restore_placeholders($translated, $ph_map);
                            } else {
                                $final = $this->restore_html_formatting($translated, $original);
                            }
                            if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                                $final = $parser->i18n_cleanup_artifacts($final);
                            }
                            // Try to restore lost links
                            if (method_exists($parser, 'restore_lost_links')) {
                                $final = $parser->restore_lost_links($final, $original);
                            }
                            $translations[$path] = $final;
                        } else {
                            AITRFOEL_Logger::log(
                                'Individual translation failed for text: ' . substr($text, 0, 50) . '...',
                                'error',
                                $page_id
                            );
                        }
                        
                        $processed++;
                        usleep($this->rate_limit_delay * 1000); // Rate limiting
                    }
                } else {
                    // Process successful batch results
                    // NOTE: batch_results is keyed by SOURCE TEXT, not numeric index
                    foreach ($batch_items as $index => $item) {
                        $text = $item['src'];
                        // Check if translation exists by source text key OR numeric index (for compatibility)
                        $translated = null;
                        if (isset($batch_results[$text])) {
                            $translated = $batch_results[$text]; // Keyed by source text
                        } elseif (isset($batch_results[$index])) {
                            $translated = $batch_results[$index]; // Fallback to numeric index
                        }
                        
                        if ($translated !== null) {
                            // Guard: if batch returned non-string or empty, fallback to individual to avoid applying raw/empty
                            if (!is_string($translated) || trim($translated) === '') {
                                AITRFOEL_Logger::log('Empty/non-string batch item detected, using individual fallback for path: ' . ($item['path'] ?? 'n/a'), 'warning', $page_id);
                                $translated = $api->translate_text($text, $target_lang, $detected_lang);
                            }
                            $path = $item['path'];
                            $original = $item['original'];
                            $ph_map = $item['ph_map'];
                            
                            // Cache the translation (only if enabled)
                            // But DON'T cache if translation is identical to source (failed translation)
                            if ($this->cache_enabled) {
                                $tr_clean = strtolower(trim(html_entity_decode(wp_strip_all_tags($translated), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                                $src_clean = strtolower(trim(html_entity_decode(wp_strip_all_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                                if ($tr_clean !== $src_clean) {
                                    $cache->set_to_cache(
                                        $text,
                                        $translated, // raw model output (placeholders still present)
                                        $target_lang,
                                        $detected_lang,
                                        get_option('aitrfoel_openai_model', 'gpt-3.5-turbo')
                                    );
                                }
                            }
                            
                            // Restore placeholders (primary) or formatting (fallback)
                            if (!empty($ph_map) && method_exists($parser, 'i18n_restore_placeholders')) {
                                $final = $parser->i18n_restore_placeholders($translated, $ph_map);
                            } else {
                                $final = $this->restore_html_formatting($translated, $original);
                            }
                            if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                                $final = $parser->i18n_cleanup_artifacts($final);
                            }
                            $translations[$path] = $final;
                        } else {
                            // No translation found in batch results - fallback to individual
                            AITRFOEL_Logger::log('Batch result missing for text (fallback to individual): ' . substr($text, 0, 50) . '...', 'warning', $page_id);
                            $translated = $api->translate_text($text, $target_lang, $detected_lang);
                            if (!is_wp_error($translated) && is_string($translated) && trim($translated) !== '') {
                                $path = $item['path'];
                                $original = $item['original'];
                                $ph_map = $item['ph_map'];
                                
                                if (!empty($ph_map) && method_exists($parser, 'i18n_restore_placeholders')) {
                                    $final = $parser->i18n_restore_placeholders($translated, $ph_map);
                                } else {
                                    $final = $this->restore_html_formatting($translated, $original);
                                }
                                if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                                    $final = $parser->i18n_cleanup_artifacts($final);
                                }
                                $translations[$path] = $final;
                            }
                        }
                        
                        $processed++;
                    }
                    
                    // No delay between successful batch calls - the server handles rate limiting
                }
                
                // Update progress
                $progress = round(($processed / $total_strings) * 100);
                AITRFOEL_Logger::log(
                    sprintf('Translation progress: %d%% (%d/%d strings processed)', 
                        $progress, $processed, $total_strings),
                    'info',
                    $page_id
                );

                // Time-budget check: break early so create_translated_page() still runs
                // and the partial result is saved to the DB before PHP is killed.
                if ((microtime(true) - $budget_start) >= $budget_seconds) {
                    $timed_out = true;
                    AITRFOEL_Logger::log(
                        sprintf('[TIMING] Time budget of %ds reached after batch %d (%d/%d strings done). Saving partial page.',
                            $budget_seconds, $batch_index, $processed, $total_strings),
                        'warning',
                        $page_id
                    );
                    break;
                }
            }

            // Process large strings individually — they bypass the batch API entirely
            // to avoid wasting the time budget on a 60-second cURL timeout.
            if (!empty($large_items) && !$timed_out) {
                AITRFOEL_Logger::log(sprintf('[TIMING] Processing %d large string(s) individually', count($large_items)), 'info', $page_id);
                foreach ($large_items as $item) {
                    if ((microtime(true) - $budget_start) >= $budget_seconds) {
                        $timed_out = true;
                        AITRFOEL_Logger::log(
                            sprintf('[TIMING] Time budget of %ds reached before large string processing. Saving partial page.', $budget_seconds),
                            'warning', $page_id
                        );
                        break;
                    }
                    $text     = $item['src'];
                    $path     = $item['path'];
                    $original = $item['original'];
                    $ph_map   = $item['ph_map'];

                    $translated = $api->translate_text($text, $target_lang, $detected_lang);

                    if (is_wp_error($translated)) {
                        $err     = $translated->get_error_message();
                        $err_code = $translated->get_error_code();
                        // chunk_timeout / word_limit_exceeded: partial progress was cached; will retry next pass
                        if ($err_code === 'word_limit_exceeded') {
                            $timed_out      = true;
                            $word_limit_hit = true; // word limit exhausted — do not auto-retry
                        } elseif ($err_code === 'chunk_timeout' ||
                            strpos($err, 'chunk_timeout') !== false || strpos($err, 'timed out') !== false) {
                            $timed_out = true;
                        }
                        AITRFOEL_Logger::log('Large string translation failed: ' . $err, 'error', $page_id);
                    } else {
                        if ($this->cache_enabled) {
                            $cache->set_to_cache($text, $translated, $target_lang, $detected_lang,
                                get_option('aitrfoel_openai_model', 'gpt-3.5-turbo'));
                        }
                        if (!empty($ph_map) && method_exists($parser, 'i18n_restore_placeholders')) {
                            $final = $parser->i18n_restore_placeholders($translated, $ph_map);
                        } else {
                            $final = $this->restore_html_formatting($translated, $original);
                        }
                        if (method_exists($parser, 'i18n_cleanup_artifacts')) {
                            $final = $parser->i18n_cleanup_artifacts($final);
                        }
                        if (method_exists($parser, 'restore_lost_links')) {
                            $final = $parser->restore_lost_links($final, $original);
                        }
                        $translations[$path] = $final;
                    }
                    $processed++;
                }
            }
        }
        
        // Final processing summary
    $cached_count = count($cached_translations);
    $translated_count = count($translations);
    $uncached_item_count = isset($uncached_items) ? count($uncached_items) : 0;
    $total_elements_processed = $cached_count + $translated_count;
        
        // Calculate statistics for this translation
        $word_count = 0;
        foreach ($strings_to_translate as $string_data) {
            if (isset($string_data['original'])) {
                $text = wp_strip_all_tags($string_data['original']);
                $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                $word_count += self::count_words($text);
            }
        }
        
        // Prepare estimated cost as fallback
        $usage_tracker = class_exists('AITRFOEL_Usage_Tracker') ? AITRFOEL_Usage_Tracker::get_instance() : null;
        // Simplified usage tracking - no cost estimation in basic version
        $estimated_cost = 0;

        // Actual usage summary from OpenAI API
        $usage_summary = method_exists($api, 'get_usage_summary') ? $api->get_usage_summary(true) : [];
        $prompt_tokens_actual = (int) ($usage_summary['prompt_tokens'] ?? 0);
        $completion_tokens_actual = (int) ($usage_summary['completion_tokens'] ?? 0);
        $total_tokens_actual = (int) ($usage_summary['total_tokens'] ?? ($prompt_tokens_actual + $completion_tokens_actual));
        $usage_records = $usage_summary['records'] ?? [];
        $usage_records_count = (int) ($usage_summary['records_count'] ?? count($usage_records));

        $has_actual_usage = ($total_tokens_actual > 0) || $usage_records_count > 0;
        $expected_usage = $uncached_item_count > 0;

        $input_tokens_final = 0;
        $output_tokens_final = 0;
        $total_tokens_final = 0;
        $cost_value = $estimated_cost;
        $cost_formatted = isset($estimated_cost_data['cost_formatted']) ? $estimated_cost_data['cost_formatted'] : ('$' . number_format($estimated_cost, 4));
        $is_estimated_stats = true;

        if ($has_actual_usage) {
            $is_estimated_stats = false;
            $input_tokens_final = $prompt_tokens_actual;
            $output_tokens_final = $completion_tokens_actual;
            $total_tokens_final = $total_tokens_actual;

            // Simplified cost calculation - no detailed cost tracking in basic version
            $cost_value = 0;
            $cost_formatted = '$0.0000';
        } elseif (!$expected_usage) {
            // All strings were served from cache
            $is_estimated_stats = false;
            $cost_value = 0;
            $cost_formatted = '$0.0000';
        } else {
            // Fallback to estimation when usage metrics are missing
            if (!empty($estimated_cost_data)) {
                $input_tokens_final = $estimated_cost_data['input_tokens'] ?? 0;
                $output_tokens_final = $estimated_cost_data['output_tokens'] ?? 0;
                $total_tokens_final = $estimated_cost_data['total_tokens'] ?? ($input_tokens_final + $output_tokens_final);
                $cost_value = $estimated_cost_data['estimated_cost'] ?? $cost_value;
                $cost_formatted = $estimated_cost_data['cost_formatted'] ?? ('$' . number_format($cost_value, 4));
            }
        }

        $usage_records_summary = [];
        if (!empty($usage_records)) {
            $usage_records_summary = array_map(function($record) {
                return [
                    'operation' => $record['operation'] ?? 'unknown',
                    'attempt' => $record['attempt'] ?? 0,
                    'prompt_tokens' => $record['prompt_tokens'] ?? 0,
                    'completion_tokens' => $record['completion_tokens'] ?? 0,
                    'total_tokens' => $record['total_tokens'] ?? 0,
                ];
            }, $usage_records);

            $encoded_records = function_exists('wp_json_encode') ? wp_json_encode($usage_records_summary) : json_encode($usage_records_summary);
            if (!empty($encoded_records)) {
                AITRFOEL_Logger::log('Usage breakdown (per call): ' . $encoded_records, 'debug', $page_id);
            }
        }

        // Get character and symbol counts from OpenAI usage records (more accurate)
        $character_count = 0;
        $symbol_count = 0;
        
        if (!empty($usage_records)) {
            // Sum up characters and symbols from all API calls
            foreach ($usage_records as $record) {
                $character_count += $record['input_characters'] ?? 0;
                $symbol_count += $record['input_symbols'] ?? 0;
            }
        }
        
        // Fallback: calculate from strings if not available from API records
        if ($character_count === 0 && class_exists('AITRFOEL_Usage_Tracker')) {
            $usage_tracker = AITRFOEL_Usage_Tracker::get_instance();
            
            // Extract original text from strings_to_translate
            $all_original_texts = [];
            foreach ($strings_to_translate as $string_data) {
                if (isset($string_data['original'])) {
                    $all_original_texts[] = $string_data['original'];
                }
            }
            
            // Basic character counting for simplified usage tracker
            $character_count = 0;
            $symbol_count = 0;
            foreach ($all_original_texts as $text) {
                $character_count += mb_strlen($text, 'UTF-8');
                // Count symbols (non-alphanumeric, non-space characters)
                $symbol_count += preg_match_all('/[^\p{L}\p{N}\s]/u', $text);
            }
            
            if (class_exists('AITRFOEL_Logger')) {
                AITRFOEL_Logger::log("Using fallback character counting: {$character_count} chars, {$symbol_count} symbols", 'debug');
            }
        } else if (class_exists('AITRFOEL_Logger')) {
            AITRFOEL_Logger::log("Using API record character counts: {$character_count} chars, {$symbol_count} symbols", 'debug');
        }

        // Store statistics
        $this->last_translation_stats = [
            'elements_translated' => $total_elements_processed,
            'word_count' => $word_count,
            'character_count' => $character_count,
            'symbol_count' => $symbol_count,
            'input_tokens' => $input_tokens_final,
            'output_tokens' => $output_tokens_final,
            'total_tokens' => $total_tokens_final,
            'estimated_cost' => $cost_value,
            'cost_formatted' => $cost_formatted,
            'is_estimated' => $is_estimated_stats,
            'cached_strings' => $cached_count,
            'uncached_strings' => $uncached_item_count,
            'usage_records' => $usage_records_summary
        ];
        
        $usage_mode_label = $is_estimated_stats ? 'estimated' : 'actual';
        AITRFOEL_Logger::log(
            sprintf(
                'Usage totals (%s): prompt=%d, completion=%d, total=%d, cost=%s, pages=%d',
                $usage_mode_label,
                $this->last_translation_stats['input_tokens'],
                $this->last_translation_stats['output_tokens'],
                $this->last_translation_stats['total_tokens'],
                $cost_formatted,
                1
            ),
            'info',
            $page_id
        );
        
        AITRFOEL_Logger::log(
            sprintf('Translation completed: %d cached, %d new translations, %d total', 
                $cached_count, $translated_count, $total_elements_processed),
            'info',
            $page_id
        );
        
        $total_batch_elapsed = round((microtime(true) - $batch_start_time) * 1000);
        AITRFOEL_Logger::log(sprintf('[TIMING] Total batch_translate_strings: %dms', $total_batch_elapsed), 'info', $page_id);
        
        // Merge cached and newly translated results
        $translations = array_merge($cached_translations, $translations);

        // Extract pre-translated title BEFORE gap-filler and apply_translations
        // so the parser doesn't try to set __aitrfoel_post_title__ as an Elementor data path
        if ($original_post && !empty($original_post->post_title)) {
            if (isset($translations[$title_batch_key])) {
                $pre_translated_title = wp_strip_all_tags($translations[$title_batch_key]);
                unset($translations[$title_batch_key]);
            } elseif ($title_already_in_batch) {
                foreach ($strings_to_translate as $s) {
                    $stripped = trim(wp_strip_all_tags($s['original'] ?? ''));
                    if ($stripped === trim($original_post->post_title) && isset($translations[$s['path']])) {
                        $pre_translated_title = wp_strip_all_tags($translations[$s['path']]);
                        break;
                    }
                }
            }
        }

        // Remove title entry from strings before gap-filler processes them
        $strings_to_translate = array_values(array_filter($strings_to_translate, function($s) use ($title_batch_key) {
            return ($s['path'] ?? '') !== $title_batch_key;
        }));

        // Gap-Filler second pass: fix missed/under-translated strings before applying
        // Skip on a timed-out pass to preserve the time budget; the retry call will run it.
        if (class_exists('AITRFOEL_Gap_Filler') && !$timed_out) {
            // Prepare minimal strings format expected by gap-filler
            $strings = $strings_to_translate;
            // Inject group_key heuristically (prefix path up to .settings)
            foreach ($strings as &$row) {
                if (is_array($row)) {
                    $p = (string)($row['path'] ?? '');
                    $g = preg_replace('~(\.settings.*)$~', '', $p);
                    if ($g === null) { $g = ''; }
                    $row['group_key'] = $g;
                }
            }
            unset($row);

            $opts = [
                'levenshtein_threshold' => (int) get_option('aitrfoel_gap_lev_threshold', 3),
                'residue_patterns' => (string) get_option('aitrfoel_gap_residue_patterns', '/\\b(Read more|Číst dál|Читать далее)\\b/ui'),
                'max_retry' => (int) get_option('aitrfoel_gap_max_retry', 1),
                'enable_postprocessor' => (bool) get_option('aitrfoel_gap_enable_postprocessor', true),
                'page_id' => $page_id,
            ];
            $metrics = AITRFOEL_Gap_Filler::run($strings, $translations, $target_lang, $detected_lang, $opts);
            AITRFOEL_Logger::log(sprintf('[EAT gap-filler] page=%d lang=%s retried=%d fixed=%d failed=%d', $page_id, $target_lang, $metrics['retried'] ?? 0, $metrics['fixed'] ?? 0, $metrics['failed'] ?? 0), 'info', $page_id);
            
            // Update cache with gap-filler corrections so future translations use good values
            if ($this->cache_enabled && ($metrics['fixed'] ?? 0) > 0) {
                $model = get_option('aitrfoel_openai_model', 'gpt-3.5-turbo');
                foreach ($strings as $s) {
                    $p = $s['path'] ?? '';
                    if (isset($translations[$p]) && !empty($translations[$p])) {
                        $src = $s['original'] ?? '';
                        $tr = $translations[$p];
                        // Only update cache if translation is meaningfully different from source
                        $src_c = strtolower(trim(html_entity_decode(wp_strip_all_tags($src), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                        $tr_c = strtolower(trim(html_entity_decode(wp_strip_all_tags($tr), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                        if ($src_c !== $tr_c) {
                            // Re-extract placeholders to get the cache key
                            if (method_exists($parser, 'i18n_extract_with_placeholders')) {
                                list($prepared_src, ) = $parser->i18n_extract_with_placeholders($src);
                            } else {
                                $prepared_src = $src;
                            }
                            $cache->set_to_cache($prepared_src, $tr, $target_lang, $detected_lang, $model);
                        }
                    }
                }
            }
        }
        
    // Apply translations back to Elementor data.
        // Reset the execution timer first: the translation + gap-filler passes may have consumed
        // most of max_execution_time, and we must not let the request die before the result is
        // saved (otherwise the words are spent but the page is never updated).
        if (function_exists('set_time_limit')) {
            @set_time_limit(120);
        }
        $translated_data = $parser->apply_translations($elementor_data_array, $translations);

        // Deep cleanup pass across all string fields to ensure no escaped tags/orphan placeholders remain
        $deepChanged = 0;
        $walker = function (&$node) use (&$walker, $parser, &$deepChanged) {
            if (is_string($node)) {
                $clean = $parser->i18n_cleanup_artifacts($node);
                if ($clean !== $node) { $node = $clean; $deepChanged++; }
                return;
            }
            if (is_array($node)) {
                foreach ($node as &$v) { $walker($v); }
                unset($v);
            }
        };
        $tmp = $translated_data;
        $walker($tmp);
        if ($deepChanged > 0) {
            AITRFOEL_Logger::log("Deep cleanup adjusted $deepChanged fields before save", 'info', $page_id);
            $translated_data = $tmp;
        }

        // Enhanced validation: ensure counts match and quality check
        if (!is_array($translated_data) || !$parser->validate_structure($translated_data)) {
            AITRFOEL_Logger::log('Validation failed after translation apply (structure). Rolling back.', 'error', $page_id);
            return new WP_Error('invalid_structure', __('Translation resulted in invalid Elementor structure.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Quality validation
        $quality_issues = $this->validate_translation_quality($translations, $target_lang);
        if (!empty($quality_issues)) {
            AITRFOEL_Logger::log('Translation quality issues detected: ' . implode(', ', $quality_issues), 'warning', $page_id);
        }
        
        // Localize internal links (href) to the target language before saving
        try {
            $this->localize_internal_links($translated_data, $target_lang);
        } catch (Exception $e) {
            AITRFOEL_Logger::log('URL localization failed: ' . $e->getMessage(), 'warning', $page_id);
        }

        // Swap nav_menu IDs in Elementor Nav Menu widgets to point to translated menus
        $menu_id_map = get_option('aitrfoel_menu_id_map', []);
        if (!empty($menu_id_map)) {
            $swapped = $this->swap_menu_ids_in_elementor_data($translated_data, $menu_id_map, $target_lang);
            if ($swapped > 0) {
                AITRFOEL_Logger::log("Swapped $swapped nav menu IDs to translated menus for $target_lang", 'info', $page_id);
            }
        }

        // Swap nested Elementor template references so translated headers/footers do not
        // keep pointing at source-language reusable templates/sections.
        $template_refs_swapped = $this->swap_elementor_template_refs_in_data($translated_data, $target_lang);
        if ($template_refs_swapped > 0) {
            AITRFOEL_Logger::log("Swapped $template_refs_swapped nested Elementor template references for $target_lang", 'info', $page_id);
        }

        // Create new page with translations
        $new_page_id = $this->create_translated_page($page_id, $translated_data, $target_lang, $detected_lang, $pre_translated_title, $timed_out);
        
        if (is_wp_error($new_page_id)) {
            AITRFOEL_Logger::log(
                'Failed to create translated page: ' . $new_page_id->get_error_message(),
                'error',
                $page_id
            );
            return $new_page_id;
        }

        // Record history immediately after page creation — before usage/analytics which can fail.
        // Status is 'partial' when we ran out of time mid-batch; the retry (fast from cache)
        // will overwrite with 'success'.
        $translation_duration = microtime(true) - $translation_start_time;
        if (class_exists('AITRFOEL_Translation_History')) {
            AITRFOEL_Translation_History::record([
                'original_post_id'   => $page_id,
                'translated_post_id' => $new_page_id,
                'source_lang'        => $detected_lang,
                'target_lang'        => $target_lang,
                'content_type'       => 'elementor',
                'word_count'         => $this->last_translation_stats['word_count'] ?? 0,
                'status'             => $timed_out ? 'partial' : 'success',
                'duration'           => round($translation_duration, 2),
            ]);
        }

        // Tag partial pages so the AJAX handler can trigger an automatic retry.
        // If the partial was caused by word limit exhaustion, mark it separately so
        // the AJAX handler can return 'limit_reached' to the JS instead of retrying.
        if ($timed_out) {
            update_post_meta($new_page_id, '_aitrfoel_partial_translation', 1);
            if ($word_limit_hit) {
                update_post_meta($new_page_id, '_aitrfoel_word_limit_hit', 1);
                AITRFOEL_Logger::log(
                    sprintf('Page %d partially translated — word limit exhausted, no auto-retry.', $new_page_id),
                    'warning',
                    $page_id
                );
            } else {
                delete_post_meta($new_page_id, '_aitrfoel_word_limit_hit');
                AITRFOEL_Logger::log(
                    sprintf('Page %d partially translated — retry will complete it from cache.', $new_page_id),
                    'warning',
                    $page_id
                );
            }
        } else {
            delete_post_meta($new_page_id, '_aitrfoel_partial_translation');
            delete_post_meta($new_page_id, '_aitrfoel_word_limit_hit');
        }

        // Log success
        AITRFOEL_Logger::log(
            sprintf('Successfully translated page %d to %s. New page ID: %d', $page_id, $target_lang, $new_page_id),
            'success',
            $page_id,
            $new_page_id
        );
        
        // All translations go through MeepTech's /translate endpoint which auto-counts words
        // server-side — no separate report-usage call is needed (it would double-count).
        // Just invalidate the cached license status so the next word-limit check is fresh.
        AITRFOEL_License_Manager_Server::get_instance()->clear_status_cache();
        
        // Track successful translation with PostHog Analytics
        if (function_exists('aitrfoel_analytics')) {
            $translation_duration = microtime(true) - $translation_start_time;
            aitrfoel_analytics()->track_translation_completed([
                'duration' => round($translation_duration, 2),
                'word_count' => $this->last_translation_stats['word_count'] ?? 0,
                'widget_count' => $this->last_translation_stats['strings_count'] ?? 0,
                'success' => true,
                'mode' => 'single',
                'model' => get_option('aitrfoel_ai_model', 'gpt-4o-mini'),
                'source_lang' => $detected_lang,
                'target_lang' => $target_lang,
            ]);
        }
        
        // Record in translation history — already done above, right after page creation.

        return $new_page_id;
    }
    
    /**
     * Translate Gutenberg (Block Editor) page
     * 
     * @param int $page_id Original page ID
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param float $translation_start_time Start time for analytics
     * @return int|WP_Error New page ID or error
     */
    private function translate_gutenberg_page($page_id, $target_lang, $source_lang = 'auto', $translation_start_time = null) {
        if (!$translation_start_time) {
            $translation_start_time = microtime(true);
        }
        
        AITRFOEL_Logger::log(
            sprintf('Starting Gutenberg translation for page %d to %s', $page_id, $target_lang),
            'info',
            $page_id
        );
        
        $post = get_post($page_id);
        if (!$post || empty($post->post_content)) {
            $this->track_translation_failure('no_content', 'No content found in Gutenberg page', $translation_start_time);
            return new WP_Error('no_content', __('No content found in this page.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        AITRFOEL_Logger::log(
            sprintf('Gutenberg page %d has %d characters of content', $page_id, strlen($post->post_content)),
            'info',
            $page_id
        );
        
        // Parse Gutenberg blocks
        $parser = AITRFOEL_Gutenberg_Parser::get_instance();
        $translation_map = $parser->parse_for_translation($page_id);
        
        AITRFOEL_Logger::log(
            sprintf('Gutenberg parser found %d translatable items', count($translation_map)),
            'info',
            $page_id
        );
        
        if (empty($translation_map)) {
            $this->track_translation_failure('no_translatable_content', 'No translatable content found in Gutenberg blocks', $translation_start_time);
            return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Convert to strings array for word limit check
        $strings_to_translate = [];
        foreach ($translation_map as $key => $data) {
            $strings_to_translate[] = $data;
        }
        
        // Word limit check (reuse existing logic)
        $word_limit_check = $this->check_word_limit($strings_to_translate, $page_id);
        if (is_wp_error($word_limit_check)) {
            AITRFOEL_Logger::log(
                'Word limit check failed: ' . $word_limit_check->get_error_message(),
                'warning',
                $page_id
            );
            return $word_limit_check;
        }
        
        // Detect source language
        $detected_lang = $this->detect_source_language_from_content($strings_to_translate, $source_lang, $page_id);
        
        AITRFOEL_Logger::log(
            sprintf('Gutenberg translation: Page %d, %s → %s, %d strings', 
                $page_id, $detected_lang, $target_lang, count($strings_to_translate)),
            'info',
            $page_id
        );
        
        // Include post title in batch to avoid a separate API call
        $post_title = $post->post_title;
        $title_key = '__aitrfoel_post_title__';
        $title_already_included = false;
        if (!empty($post_title)) {
            // Check if title already exists in parsed content (e.g. as H1)
            foreach ($strings_to_translate as $s) {
                $stripped = trim(wp_strip_all_tags($s['original'] ?? ''));
                if ($stripped === trim($post_title)) {
                    $title_already_included = true;
                    break;
                }
            }
            if (!$title_already_included) {
                $strings_to_translate[] = [
                    'original' => $post_title,
                    'path' => $title_key,
                    'type' => 'title',
                    'widget_type' => 'gutenberg',
                ];
            }
        }
        
        // Translate strings (title included in batch)
        $translations = $this->batch_translate_strings($strings_to_translate, $target_lang, $detected_lang, $page_id);
        
        if (is_wp_error($translations)) {
            AITRFOEL_Logger::log(
                'Batch translation failed: ' . $translations->get_error_message(),
                'error',
                $page_id
            );
            return $translations;
        }
        
        // Extract translated title from batch results
        // batch_translate_strings keys results by original text, not by path
        $translated_title = null;
        if (!$title_already_included && isset($translations[$post_title])) {
            // Title was added as a separate batch entry — keyed by original text
            $translated_title = $translations[$post_title];
            unset($translations[$post_title]); // Don't pass to apply_translations
        } elseif ($title_already_included) {
            // Title was already in content (e.g. as H1) — extract translated version
            foreach ($strings_to_translate as $s) {
                $stripped = trim(wp_strip_all_tags($s['original'] ?? ''));
                if ($stripped === trim($post_title) && isset($translations[$s['original']])) {
                    $translated_title = wp_strip_all_tags($translations[$s['original']]);
                    break;
                }
            }
        }
        
        AITRFOEL_Logger::log(
            sprintf('Received %d translations from API (translated_title: %s)', count($translations), $translated_title ? 'yes' : 'no'),
            'info',
            $page_id
        );
        
        // Apply translations to content
        $original_content = $post->post_content;
        $translated_content = $parser->apply_translations($original_content, $translations);
        
        // Create translated page (pass pre-translated title to skip separate API call)
        $new_page_id = $this->create_translated_page_generic($page_id, $translated_content, $target_lang, $detected_lang, 'gutenberg', $translated_title);
        
        if (is_wp_error($new_page_id)) {
            AITRFOEL_Logger::log(
                'Failed to create translated page: ' . $new_page_id->get_error_message(),
                'error',
                $page_id
            );
            return $new_page_id;
        }
        
        // Track success
        $this->track_translation_success($page_id, $new_page_id, $target_lang, $detected_lang, $translation_start_time, 'gutenberg');
        
        AITRFOEL_Logger::log(
            sprintf('Gutenberg translation complete: Page %d -> %d', $page_id, $new_page_id),
            'success',
            $page_id
        );
        
        return $new_page_id;
    }
    
    /**
     * Translate Classic Editor page
     * 
     * @param int $page_id Original page ID
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param float $translation_start_time Start time for analytics
     * @return int|WP_Error New page ID or error
     */
    private function translate_classic_page($page_id, $target_lang, $source_lang = 'auto', $translation_start_time = null) {
        if (!$translation_start_time) {
            $translation_start_time = microtime(true);
        }
        
        $post = get_post($page_id);
        if (!$post) {
            $this->track_translation_failure('no_content', 'Post not found', $translation_start_time);
            return new WP_Error('no_content', __('No content found in this page.', 'ai-translator-for-elementor-and-polylang'));
        }

        // Parse Classic Editor content. Pages with no body (e.g. WooCommerce shop/cart/
        // checkout pages) are allowed through with an empty map — the title will be
        // translated below and a shell page created for the language switcher.
        $parser = AITRFOEL_Classic_Parser::get_instance();
        $has_body = !empty(trim($post->post_content));
        $translation_map = $has_body ? $parser->parse_for_translation($page_id) : [];

        if (empty($translation_map) && !$has_body) {
            // Title-only page: allowed if there is a non-empty title to translate.
            if (empty(trim($post->post_title))) {
                $this->track_translation_failure('no_translatable_content', 'No translatable content found in Classic Editor', $translation_start_time);
                return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
            }
            // Proceed with empty map; the title will be added to the batch below.
        } elseif (empty($translation_map)) {
            $this->track_translation_failure('no_translatable_content', 'No translatable content found in Classic Editor', $translation_start_time);
            return new WP_Error('no_translatable_content', __('No translatable content found in this page.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Convert to strings array for word limit check
        $strings_to_translate = [];
        foreach ($translation_map as $key => $data) {
            $strings_to_translate[] = $data;
        }
        
        // Word limit check (reuse existing logic)
        $word_limit_check = $this->check_word_limit($strings_to_translate, $page_id);
        if (is_wp_error($word_limit_check)) {
            return $word_limit_check;
        }
        
        // Detect source language
        $detected_lang = $this->detect_source_language_from_content($strings_to_translate, $source_lang, $page_id);
        
        AITRFOEL_Logger::log(
            sprintf('Classic translation: Page %d, %s → %s, %d strings', 
                $page_id, $detected_lang, $target_lang, count($strings_to_translate)),
            'info',
            $page_id
        );
        
        // Include post title in batch to avoid a separate API call
        $post_title = $post->post_title;
        $title_key = '__aitrfoel_post_title__';
        if (!empty($post_title)) {
            // Check if title already exists in parsed content (e.g. as H1)
            $title_already_included = false;
            foreach ($strings_to_translate as $s) {
                $stripped = trim(wp_strip_all_tags($s['original'] ?? ''));
                if ($stripped === trim($post_title)) {
                    $title_already_included = true;
                    break;
                }
            }
            if (!$title_already_included) {
                $strings_to_translate[] = [
                    'original' => $post_title,
                    'path' => $title_key,
                    'type' => 'title',
                    'widget_type' => 'classic_editor',
                ];
            }
        }
        
        // Translate strings (title included in batch)
        $translations = $this->batch_translate_strings($strings_to_translate, $target_lang, $detected_lang, $page_id);
        
        if (is_wp_error($translations)) {
            return $translations;
        }
        
        // Extract translated title from batch results
        // batch_translate_strings() keys translations by original text, not path
        $translated_title = null;
        if (isset($translations[$post_title])) {
            $translated_title = $translations[$post_title];
        } elseif (!empty($title_already_included)) {
            // Title was already in content (e.g. as H1) — extract translated version
            foreach ($strings_to_translate as $s) {
                $stripped = trim(wp_strip_all_tags($s['original'] ?? ''));
                if ($stripped === trim($post_title) && isset($translations[$s['original']])) {
                    $translated_title = wp_strip_all_tags($translations[$s['original']]);
                    break;
                }
            }
        }
        
        // Apply translations to content
        $original_content = $post->post_content;
        $translated_content = $parser->apply_translations($original_content, $translations);
        
        // Create translated page (pass pre-translated title to skip separate API call)
        $new_page_id = $this->create_translated_page_generic($page_id, $translated_content, $target_lang, $detected_lang, 'classic', $translated_title);
        
        if (is_wp_error($new_page_id)) {
            return $new_page_id;
        }
        
        // Track success
        $this->track_translation_success($page_id, $new_page_id, $target_lang, $detected_lang, $translation_start_time, 'classic');
        
        return $new_page_id;
    }
    
    /**
     * Translate a WooCommerce product that has no traditional editor content.
     * Handles products with only title, short description, attributes, etc.
     * 
     * @param int $page_id Original product ID
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param float $translation_start_time Start time for analytics
     * @return int|WP_Error New product ID or error
     */
    private function translate_woocommerce_product($page_id, $target_lang, $source_lang = 'auto', $translation_start_time = null) {
        if (!$translation_start_time) {
            $translation_start_time = microtime(true);
        }
        
        $post = get_post($page_id);
        if (!$post) {
            $this->track_translation_failure('no_content', 'Product not found', $translation_start_time);
            return new WP_Error('no_content', __('Product not found.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Collect translatable texts: title + short description + long description
        $texts_to_translate = [];
        $text_keys = [];
        
        if (!empty($post->post_title)) {
            $texts_to_translate[] = $post->post_title;
            $text_keys[] = 'title';
        }
        
        if (!empty(trim($post->post_excerpt))) {
            $texts_to_translate[] = $post->post_excerpt;
            $text_keys[] = 'excerpt';
        }
        
        if (!empty(trim($post->post_content))) {
            $texts_to_translate[] = $post->post_content;
            $text_keys[] = 'content';
        }
        
        if (empty($texts_to_translate)) {
            $this->track_translation_failure('no_translatable_content', 'Product has no translatable text', $translation_start_time);
            return new WP_Error('no_translatable_content', __('This product has no translatable content (no title or short description).', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Word limit check
        $strings_for_check = [];
        foreach ($texts_to_translate as $i => $text) {
            $strings_for_check[] = [
                'original' => $text,
                'path' => $text_keys[$i],
                'type' => 'text',
                'widget_type' => 'woocommerce_product',
            ];
        }
        $word_limit_check = $this->check_word_limit($strings_for_check, $page_id);
        if (is_wp_error($word_limit_check)) {
            return $word_limit_check;
        }
        
        // Detect source language
        $detected_lang = $this->detect_source_language_from_content($strings_for_check, $source_lang, $page_id);
        
        AITRFOEL_Logger::log(
            sprintf('WooCommerce product translation: Product %d, %s → %s, %d texts', 
                $page_id, $detected_lang, $target_lang, count($texts_to_translate)),
            'info',
            $page_id
        );
        
        // Batch-translate title and excerpt
        $api = AITRFOEL_OpenAI_API::get_instance();
        $translated_texts = $api->translate_batch($texts_to_translate, $target_lang, $detected_lang);
        
        if (is_wp_error($translated_texts)) {
            $this->track_translation_failure('translation_failed', 'API translation failed', $translation_start_time);
            return $translated_texts;
        }
        
        // Map results back using original texts as keys
        // translate_batch returns [ original_text => translated_text ]
        $translated_title = null;
        $translated_excerpt = null;
        $translated_content = null;
        foreach ($texts_to_translate as $i => $original_text) {
            $translated = isset($translated_texts[$original_text]) ? $translated_texts[$original_text] : null;
            if (!$translated || is_wp_error($translated)) {
                continue;
            }
            if ($text_keys[$i] === 'title') {
                $translated_title = $translated;
            } elseif ($text_keys[$i] === 'excerpt') {
                $translated_excerpt = $translated;
            } elseif ($text_keys[$i] === 'content') {
                $translated_content = $translated;
            }
        }
        
        if (!$translated_title) {
            $translated_title = $post->post_title . ' (' . strtoupper($target_lang) . ')';
        }
        
        // Create translated product using the generic path
        $new_page_id = $this->create_translated_page_generic(
            $page_id, 
            $translated_content ?: '', // translated long description
            $target_lang, 
            $detected_lang, 
            'woocommerce', 
            $translated_title
        );
        
        if (is_wp_error($new_page_id)) {
            return $new_page_id;
        }
        
        // Apply translated excerpt (the generic path's translate_post_meta_fields handles SEO/WC meta,
        // but the excerpt was already translated above via batch, so apply it directly)
        if ($translated_excerpt) {
            wp_update_post(['ID' => $new_page_id, 'post_excerpt' => $translated_excerpt]);
        }
        
        // Track success
        $this->track_translation_success($page_id, $new_page_id, $target_lang, $detected_lang, $translation_start_time, 'woocommerce');
        
        return $new_page_id;
    }
    
    /**
     * Check word limit for translation
     * 
     * @param array $strings_to_translate Strings to translate
     * @param int $page_id Page ID for logging
     * @return true|WP_Error True if within limit, error otherwise
     */
    private function check_word_limit($strings_to_translate, $page_id) {
        $license_manager = AITRFOEL_License_Manager_Server::get_instance();
        $license_status = $license_manager->get_license_status();
        
        $plan = $license_status['plan'];
        $word_limit = $license_status['word_limit'];
        $words_left = $license_status['words_left'];
        
        // Calculate estimated word count
        // Server counts both source + translated words, so apply 2× factor
        $source_words = 0;
        foreach ($strings_to_translate as $string_data) {
            if (isset($string_data['original'])) {
                $text = wp_strip_all_tags($string_data['original']);
                $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                $source_words += self::count_words($text);
            }
        }
        $server_factor = defined('AITRFOEL_Translate_All::SERVER_USAGE_FACTOR') ? AITRFOEL_Translate_All::SERVER_USAGE_FACTOR : 2;
        $estimated_words = $source_words * $server_factor;
        
        if ($word_limit !== -1 && $estimated_words > $words_left) {
            // Check cooldown for free plan
            if (strtolower($plan) === 'free') {
                $cooldown_end = get_option('aitrfoel_cooldown_end_date', 0);
                if ($cooldown_end > time()) {
                    $days_left = ceil(($cooldown_end - time()) / DAY_IN_SECONDS);
                    return new WP_Error(
                        'cooldown_active',
                        sprintf(
                            __('You have reached your monthly limit. Free plan users must wait %d more days for the limit to reset.', 'ai-translator-for-elementor-and-polylang'),
                            $days_left
                        )
                    );
                }
            }
            
            return new WP_Error(
                'insufficient_words',
                sprintf(
                    __('This translation requires approximately %s words, but you only have %s words remaining.', 'ai-translator-for-elementor-and-polylang'),
                    number_format($estimated_words),
                    number_format($words_left)
                )
            );
        }
        
        return true;
    }
    
    /**
     * Detect source language from content
     * 
     * @param array $strings_to_translate Strings to analyze
     * @param string $source_lang Provided source language (or 'auto')
     * @param int $page_id Page ID for logging
     * @return string Detected language code
     */
    private function detect_source_language_from_content($strings_to_translate, $source_lang, $page_id) {
        if ($source_lang !== 'auto') {
            return $source_lang;
        }

        $detected = $this->detect_source_language($strings_to_translate, $page_id);
        if ($detected) {
            return $detected;
        }

        // Content too short/empty to auto-detect (e.g. a meta-only JetEngine CPT whose
        // post body is empty). Fall back to the source post's assigned language, then
        // the site's default language, before the last-resort 'en'. Without this, a
        // Dutch-default site translates "nl → en" as "en → en" — a no-op that leaves
        // titles and custom fields untranslated.
        if (class_exists('AITRFOEL_Multilingual_Manager')) {
            $mm = AITRFOEL_Multilingual_Manager::get_instance();
            if ($mm) {
                if (method_exists($mm, 'get_post_language')) {
                    $assigned = $mm->get_post_language($page_id);
                    if ($assigned) {
                        return $assigned;
                    }
                }
                if (method_exists($mm, 'get_default_language')) {
                    $default = $mm->get_default_language();
                    if ($default) {
                        return $default;
                    }
                }
            }
        }

        return 'en';
    }
    
    /**
     * Batch translate strings using the OpenAI API
     * 
     * @param array $strings_to_translate Array of string data with 'original' key
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param int $page_id Page ID for logging
     * @return array|WP_Error Array of original => translated, or error
     */
    private function batch_translate_strings($strings_to_translate, $target_lang, $source_lang, $page_id) {
        $api = AITRFOEL_OpenAI_API::get_instance();
        $cache = AITRFOEL_Cache_Manager::get_instance();
        
        $translations = [];
        $uncached_strings = [];
        
        AITRFOEL_Logger::log(
            sprintf('batch_translate_strings: Processing %d strings for translation', count($strings_to_translate)),
            'info',
            $page_id
        );
        
        // Check cache first
        foreach ($strings_to_translate as $data) {
            $original = isset($data['original']) ? $data['original'] : '';
            if (empty(trim($original))) {
                continue;
            }
            
            $cached = null;
            if ($this->cache_enabled) {
                $cached = $cache->get_from_cache($original, $target_lang, $source_lang);
            }
            
            if ($cached) {
                $translations[$original] = $cached;
            } else {
                $uncached_strings[] = $original;
            }
        }
        
        AITRFOEL_Logger::log(
            sprintf('batch_translate_strings: %d cached, %d need translation', 
                count($translations), count($uncached_strings)),
            'info',
            $page_id
        );
        
        // Batch translate uncached strings
        if (!empty($uncached_strings)) {
            // Split by word count (~1500 words per batch) to avoid API timeouts on large content
            $max_words_per_batch = 1500;
            $batches = [];
            $current_batch = [];
            $current_words = 0;
            
            foreach ($uncached_strings as $str) {
                $wc = self::count_words(wp_strip_all_tags($str));
                if (!empty($current_batch) && ($current_words + $wc) > $max_words_per_batch) {
                    $batches[] = $current_batch;
                    $current_batch = [];
                    $current_words = 0;
                }
                $current_batch[] = $str;
                $current_words += $wc;
            }
            if (!empty($current_batch)) {
                $batches[] = $current_batch;
            }
            
            AITRFOEL_Logger::log(
                sprintf('batch_translate_strings: %d batches (~%d words/batch)%s', count($batches), $max_words_per_batch, count($batches) > 1 ? ' — using PARALLEL requests' : ''),
                'info',
                $page_id
            );
            
            // Use parallel requests when multiple batches exist
            if (count($batches) > 1) {
                $parallel_result = $api->translate_batches_parallel($batches, $target_lang, $source_lang);
                
                if (!is_wp_error($parallel_result)) {
                    foreach ($parallel_result as $original => $translated) {
                        $translations[$original] = $translated;
                        
                        if ($this->cache_enabled && !empty($translated)) {
                            $cache->set_to_cache($original, $translated, $target_lang, $source_lang, 'gpt-4o-mini');
                        }
                    }
                } else {
                    AITRFOEL_Logger::log('Parallel batch error: ' . $parallel_result->get_error_message(), 'error', $page_id);
                }
            } else {
                // Single batch — use normal sequential path
                $batch = $batches[0];
                $batch_wc = 0;
                foreach ($batch as $s) { $batch_wc += self::count_words(wp_strip_all_tags($s)); }
                AITRFOEL_Logger::log(
                    sprintf('batch_translate_strings: Processing 1 batch with %d strings (~%d words)', count($batch), $batch_wc),
                    'info',
                    $page_id
                );
                
                $result = $api->translate_batch($batch, $target_lang, $source_lang);
                
                if (!is_wp_error($result)) {
                    foreach ($result as $original => $translated) {
                        $translations[$original] = $translated;
                        
                        if ($this->cache_enabled && !empty($translated)) {
                            $cache->set_to_cache($original, $translated, $target_lang, $source_lang, 'gpt-4o-mini');
                        }
                    }
                } else {
                    AITRFOEL_Logger::log('Batch translation error: ' . $result->get_error_message(), 'error', $page_id);
                }
            }
        }
        
        // Track word count
        $word_count = 0;
        foreach ($translations as $original => $translated) {
            $word_count += self::count_words(wp_strip_all_tags($original));
        }
        $this->last_translation_stats['word_count'] = $word_count;
        $this->last_translation_stats['strings_count'] = count($translations);
        
        AITRFOEL_Logger::log(
            sprintf('batch_translate_strings: Complete. %d translations, %d words', 
                count($translations), $word_count),
            'info',
            $page_id
        );
        
        return $translations;
    }
    
    /**
     * Create translated page for Gutenberg/Classic content
     * 
     * @param int $original_page_id Original page ID
     * @param string $translated_content Translated post content
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param string $content_type Content type (gutenberg/classic)
     * @return int|WP_Error New page ID or error
     */
    private function get_translated_parent_id($parent_id, $target_lang) {
        $parent_id = (int) $parent_id;
        if (!$parent_id) {
            return 0;
        }
        if (class_exists('AITRFOEL_Builtin_Integration') && AITRFOEL_Builtin_Integration::is_enabled()) {
            $translations = AITRFOEL_Builtin_Integration::get_instance()->get_post_translations($parent_id);
            if (!empty($translations[$target_lang])) {
                return (int) $translations[$target_lang];
            }
        }
        if (function_exists('pll_get_post')) {
            $tp = pll_get_post($parent_id, $target_lang);
            if ($tp) {
                return (int) $tp;
            }
        }
        $wpml = apply_filters('wpml_object_id', $parent_id, get_post_type($parent_id), false, $target_lang);
        if ($wpml && (int) $wpml !== $parent_id) {
            return (int) $wpml;
        }
        return $parent_id; // no translated parent yet — keep under the source parent
    }

    private function create_translated_page_generic($original_page_id, $translated_content, $target_lang, $source_lang, $content_type, $pre_translated_title = null) {
        $original_page = get_post($original_page_id);
        
        if (!$original_page) {
            return new WP_Error('page_not_found', __('Original page not found.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Check if a translation already exists for this language — update instead of duplicating
        $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
        $existing_translations = $multilingual->is_active() ? $multilingual->get_post_translations($original_page_id) : [];
        
        // Normalize target_lang to code for lookup (translations are keyed by code)
        $target_code = $target_lang;
        if (strlen($target_lang) > 3 && class_exists('AITRFOEL_Language_Mapper')) {
            $resolved = AITRFOEL_Language_Mapper::get_code_from_language_name($target_lang);
            if ($resolved) {
                $target_code = $resolved;
            }
        }

        // Rewrite [elementor-template id="X"] shortcodes in the translated post_content
        // so each id points at the X-language translation of template X. This makes
        // the translated post self-contained and immune to render paths that bypass
        // our pre_do_shortcode_tag runtime swap (which still acts as a safety net).
        if (is_string($translated_content) && $translated_content !== '') {
            $translated_content = $this->rewrite_elementor_template_shortcode_ids($translated_content, $target_code);
        }

        $existing_page_id = isset($existing_translations[$target_code]) ? (int) $existing_translations[$target_code] : 0;

        // Fallback: if the multilingual plugin didn't register the relationship (e.g. on a
        // partial-save retry), search for a previously created page by our own tracking meta.
        if (!$existing_page_id || !get_post($existing_page_id)) {
            $fallback_posts = get_posts([
                'post_type'              => $original_page->post_type,
                'post_status'            => ['publish', 'draft', 'private', 'pending'],
                'posts_per_page'         => 1,
                'fields'                 => 'ids',
                'no_found_rows'          => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false,
                'meta_query'             => [[
                    'key'     => '_aitrfoel_translated_from',
                    'value'   => $original_page_id . '_' . $target_code,
                    'compare' => '=',
                ]],
            ]);
            if (!empty($fallback_posts)) {
                $existing_page_id = (int) $fallback_posts[0];
                AITRFOEL_Logger::log(
                    sprintf('Fallback lookup: found existing %s translation (ID %d) for page %d → %s via tracking meta.', $content_type, $existing_page_id, $original_page_id, $target_lang),
                    'info',
                    $original_page_id
                );
            }
        }

        if ($existing_page_id && get_post($existing_page_id)) {
            AITRFOEL_Logger::log(
                sprintf('Existing %s translation found (ID %d) for page %d → %s. Updating.', $content_type, $existing_page_id, $original_page_id, $target_lang),
                'info',
                $original_page_id
            );
            
            // Translate title
            if (!empty($pre_translated_title) && is_string($pre_translated_title)) {
                $api = AITRFOEL_OpenAI_API::get_instance();
                $translated_title = method_exists($api, 'clean_html_from_text') 
                    ? $api->clean_html_from_text($pre_translated_title) 
                    : wp_strip_all_tags($pre_translated_title);
            } else {
                $api = AITRFOEL_OpenAI_API::get_instance();
                $translated_title = $api->translate_title($original_page->post_title, $target_lang, $source_lang);
            }
            
            // Update existing page
            $update_args = [
                'ID' => $existing_page_id,
                'post_content' => $translated_content,
            ];
            if (!is_wp_error($translated_title) && !empty($translated_title)) {
                $update_args['post_title'] = $translated_title;

                // Optionally update slug from translated title.
                if (get_option('aitrfoel_translate_url_slugs', true)) {
                    $translated_slug = sanitize_title($translated_title);
                    if (!empty($translated_slug)) {
                        $update_args['post_name'] = $translated_slug;
                    }
                }
            }
            wp_update_post($update_args);
            
            // Copy meta
            $meta_keys_to_copy = [
                '_wp_page_template', 'site-post-title', 'ast-title-bar-display',
                'ast-featured-img', 'ast-site-content-layout', 'site-sidebar-layout',
            ];
            foreach ($meta_keys_to_copy as $meta_key) {
                $meta_value = get_post_meta($original_page_id, $meta_key, true);
                if ($meta_value !== '') {
                    update_post_meta($existing_page_id, $meta_key, $meta_value);
                }
            }
            update_post_meta($existing_page_id, '_aitrfoel_content_type', $content_type);

            // Translate SEO meta, excerpt, and WooCommerce fields
            $this->translate_post_meta_fields($original_page_id, $existing_page_id, $target_lang, $source_lang);

            // Assign translated WooCommerce product categories/tags
            $this->translate_product_taxonomies($original_page_id, $existing_page_id, $target_lang);

            // Translate WooCommerce product attributes
            $this->translate_product_attributes($original_page_id, $existing_page_id, $target_lang, $source_lang);

            // Translate product variations for variable products
            $this->translate_product_variations($original_page_id, $existing_page_id, $target_lang, $source_lang);

            // Translate product gallery image alt text/captions
            $this->translate_product_gallery_meta($original_page_id, $existing_page_id, $target_lang, $source_lang);

            // Translate and assign post categories/tags (non-WooCommerce)
            $this->translate_post_taxonomies($original_page_id, $existing_page_id, $target_lang);
            
            return $existing_page_id;
        }
        
        // Use pre-translated title if provided (already included in batch), otherwise translate separately
        if (!empty($pre_translated_title) && is_string($pre_translated_title)) {
            // Clean HTML from title (same as translate_title does)
            $api = AITRFOEL_OpenAI_API::get_instance();
            $translated_title = method_exists($api, 'clean_html_from_text') 
                ? $api->clean_html_from_text($pre_translated_title) 
                : wp_strip_all_tags($pre_translated_title);
        } else {
            // Fallback: translate title separately (adds ~2-5s)
            $api = AITRFOEL_OpenAI_API::get_instance();
            $translated_title = $api->translate_title($original_page->post_title, $target_lang, $source_lang);
        }
        
        if (is_wp_error($translated_title) || empty($translated_title)) {
            $translated_title = $original_page->post_title . ' (' . strtoupper($target_lang) . ')';
        }
        
        // Create new page
        $target_status = get_option('aitrfoel_translated_page_status', 'draft');
        
        // If 'inherit', match the original post's status
        if ($target_status === 'inherit') {
            $target_status = ($original_page->post_status === 'publish') ? 'publish' : 'draft';
        }
        
        // Optionally generate a translated slug; otherwise preserve source-style slug.
        if (get_option('aitrfoel_translate_url_slugs', true)) {
            $translated_slug = sanitize_title($translated_title);
            if (empty($translated_slug)) {
                $translated_slug = $original_page->post_name . '-' . $target_lang;
            }
        } else {
            $translated_slug = $original_page->post_name . '-' . $target_lang;
        }
        
        $new_page_args = [
            'post_title' => $translated_title,
            'post_name' => $translated_slug,
            'post_content' => $translated_content,
            'post_status' => $target_status,
            'post_type' => $original_page->post_type,
            'post_parent' => $this->get_translated_parent_id($original_page->post_parent, $target_lang),
            'menu_order' => $original_page->menu_order,
            'post_date' => $original_page->post_date,
            'post_date_gmt' => $original_page->post_date_gmt,
            'post_excerpt' => $original_page->post_excerpt,
        ];
        
        $new_page_id = wp_insert_post($new_page_args);
        
        if (is_wp_error($new_page_id)) {
            return $new_page_id;
        }

        // Reassert the requested status after insertion in case another hook/plugin
        // forced a different post_status during wp_insert_post().
        if (get_post_status($new_page_id) !== $target_status) {
            wp_update_post([
                'ID' => $new_page_id,
                'post_status' => $target_status,
            ]);
            clean_post_cache($new_page_id);
        }
        
        // Copy relevant meta
        $meta_keys_to_copy = [
            '_wp_page_template',
            'site-post-title',
            'ast-title-bar-display',
            'ast-featured-img',
            'ast-site-content-layout',
            'site-sidebar-layout',
        ];
        
        foreach ($meta_keys_to_copy as $meta_key) {
            $meta_value = get_post_meta($original_page_id, $meta_key, true);
            if ($meta_value !== '') {
                update_post_meta($new_page_id, $meta_key, $meta_value);
            }
        }
        
        // Store content type for reference
        update_post_meta($new_page_id, '_aitrfoel_content_type', $content_type);
        update_post_meta($new_page_id, '_aitrfoel_source_page', $original_page_id);

        // Store source-page + language so we can find this page on partial-save retries
        // even when the multilingual plugin hasn't committed the relationship yet.
        update_post_meta($new_page_id, '_aitrfoel_translated_from', $original_page_id . '_' . $target_code);
        
        // Translate SEO meta, excerpt, and WooCommerce fields
        $this->translate_post_meta_fields($original_page_id, $new_page_id, $target_lang, $source_lang);

        // Setup multilingual relationship
        $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
        if ($multilingual->is_active()) {
            $result = $multilingual->setup_translation_relationship($original_page_id, $new_page_id, $target_lang);
            
            if (is_wp_error($result)) {
                AITRFOEL_Logger::log(
                    'Multilingual setup failed: ' . $result->get_error_message(),
                    'warning',
                    $original_page_id,
                    $new_page_id
                );
            }
        }
        
        // Assign translated WooCommerce product categories/tags
        $this->translate_product_taxonomies($original_page_id, $new_page_id, $target_lang);

        // Translate WooCommerce product attributes
        $this->translate_product_attributes($original_page_id, $new_page_id, $target_lang, $source_lang);

        // Translate product variations for variable products
        $this->translate_product_variations($original_page_id, $new_page_id, $target_lang, $source_lang);

        // Translate product gallery image alt text/captions
        $this->translate_product_gallery_meta($original_page_id, $new_page_id, $target_lang, $source_lang);

        // Translate and assign post categories/tags (non-WooCommerce)
        $this->translate_post_taxonomies($original_page_id, $new_page_id, $target_lang);
        
        AITRFOEL_Logger::log(
            sprintf('Created %s translated page: %d -> %d (%s)', $content_type, $original_page_id, $new_page_id, $target_lang),
            'success',
            $original_page_id,
            $new_page_id
        );
        
        return $new_page_id;
    }
    
    /**
     * Track translation success with analytics
     * 
     * @param int $page_id Original page ID
     * @param int $new_page_id New page ID
     * @param string $target_lang Target language
     * @param string $source_lang Source language
     * @param float $translation_start_time Start time
     * @param string $content_type Content type
     */
    private function track_translation_success($page_id, $new_page_id, $target_lang, $source_lang, $translation_start_time, $content_type) {
        AITRFOEL_Logger::log(
            sprintf('Successfully translated page %d to %s. New page ID: %d (type: %s)', $page_id, $target_lang, $new_page_id, $content_type),
            'success',
            $page_id,
            $new_page_id
        );
        
        // All translations go through MeepTech's /translate endpoint which auto-counts words.
        // Invalidate cached license status so next word-limit check fetches fresh data.
        AITRFOEL_License_Manager_Server::get_instance()->clear_status_cache();
        
        // Record in translation history
        $translation_duration = microtime(true) - $translation_start_time;
        if (class_exists('AITRFOEL_Translation_History')) {
            AITRFOEL_Translation_History::record([
                'original_post_id'   => $page_id,
                'translated_post_id' => $new_page_id,
                'source_lang'        => $source_lang,
                'target_lang'        => $target_lang,
                'content_type'       => $content_type,
                'word_count'         => $this->last_translation_stats['word_count'] ?? 0,
                'status'             => 'success',
                'duration'           => round($translation_duration, 2),
            ]);
        }

        // Track with PostHog Analytics
        if (function_exists('aitrfoel_analytics')) {
            aitrfoel_analytics()->track_translation_completed([
                'duration' => round($translation_duration, 2),
                'word_count' => $this->last_translation_stats['word_count'] ?? 0,
                'widget_count' => $this->last_translation_stats['strings_count'] ?? 0,
                'success' => true,
                'mode' => 'single',
                'content_type' => $content_type,
                'source_lang' => $source_lang,
                'target_lang' => $target_lang,
            ]);
        }
    }
    
    /**
     * Clean text for translation (remove HTML but preserve structure)
     * 
     * @param string $text Original text with HTML
     * @return string Clean text
     */
    private function clean_text_for_translation($text) {
        // Store original text for hash key
        $original_text = $text;
        
        // Ensure input is string
        if (!is_string($text)) {
            return '';
        }
        
        // Skip if text is too short to be meaningful (use mb_strlen for UTF-8)
        if (mb_strlen(trim($text), 'UTF-8') < 2) {
            return '';
        }
        
        // Preserve emojis and special Unicode characters
        // Convert to UTF-8 if needed
        if (!mb_check_encoding($text, 'UTF-8')) {
            $text = mb_convert_encoding($text, 'UTF-8', mb_detect_encoding($text));
        }
        
        // Preserve important line breaks and spacing elements with placeholders
        $html_placeholders = [];
        $placeholder_counter = 0;
        
        // Preserve complete HTML elements while cleaning content
        // Process in order that preserves nested structures correctly
        // SIMPLIFIED APPROACH: Only preserve major structural elements, let OpenAI handle inline formatting
        $preserved_patterns = [
            '/<(div)([^>]*)>(.*?)<\/\1>/s' => 'DIV',
            '/<(h[1-6])([^>]*)>(.*?)<\/\1>/s' => 'HDG',
            '/<(p)([^>]*)>(.*?)<\/\1>/s' => 'PGR',
            '/<(ul|ol)([^>]*)>(.*?)<\/\1>/s' => 'LST', 
            '/<(li)([^>]*)>(.*?)<\/\1>/s' => 'ITM',
        ];
        
        // Store HTML structure and replace with placeholders
        // Only for BLOCK elements - let inline elements pass through to OpenAI
        foreach ($preserved_patterns as $pattern => $placeholder_prefix) {
            AITRFOEL_Logger::log("Processing pattern for: $placeholder_prefix", 'debug');
            
            $match_count = 0;
            $text = preg_replace_callback($pattern, function($matches) use (&$html_placeholders, &$placeholder_counter, $placeholder_prefix, &$match_count) {
                $match_count++;
                $tag = $matches[1];
                $attributes = $matches[2];
                $content = $matches[3];
                
                // Create translation-resistant placeholder using random string
                $random_id = substr(md5(uniqid() . $placeholder_counter), 0, 8);
                $placeholder = $placeholder_prefix . '_' . $random_id;
                $placeholder_counter++;
                
                // Store the HTML structure with the base placeholder name
                $html_placeholders[$placeholder] = [
                    'tag' => $tag,
                    'attributes' => $attributes,
                    'is_self_closing' => false
                ];
                
                AITRFOEL_Logger::log("Match $match_count: Storing placeholder: $placeholder for tag: <$tag$attributes>", 'debug');
                
                // For all block elements, use newlines
                return "\n≪" . $placeholder . "≫\n" . $content . "\n≪/" . $placeholder . "≫\n";
            }, $text);
            
            AITRFOEL_Logger::log("Pattern $placeholder_prefix found $match_count matches", 'debug');
        }
        
        // Handle self-closing tags with translation-resistant markers (inline, no newlines)
        // Case-insensitive, handles attributes like <br class=""> and variants like <BR/>
        $text = preg_replace('/<br\s*\/?>/i', '≪BR≫', $text);
        
        // Strip HTML tags but preserve content and placeholders
        $text = wp_strip_all_tags($text);
        
        // Decode HTML entities (preserve Unicode)
        $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        
        // Clean up whitespace but preserve intentional line breaks and placeholders
        $text = preg_replace('/[ \t]+/u', ' ', $text); // Normalize spaces and tabs (with Unicode flag)
        $text = preg_replace('/\n\s*\n/u', "\n", $text); // Remove excess line breaks
        $text = trim($text);
        
        // Skip text that's only numbers, special characters, or whitespace
        // BUT allow Unicode letters (Cyrillic, Arabic, Chinese, etc.)
        if (!preg_match('/\p{L}/u', $text)) {
            return '';
        }
        
        // Store the HTML placeholders for restoration using original text hash
        $text_hash = md5($original_text);
        $this->html_placeholders_storage[$text_hash] = $html_placeholders;
        
        AITRFOEL_Logger::log("Stored placeholders for hash: $text_hash (original text hash)", 'debug');
        
        return $text;
    }
    
    /**
     * Restore HTML formatting after translation
     * 
     * @param string $translated_text Clean translated text
     * @param string $original_text Original text with HTML
     * @return string Translated text with HTML restored
     */
    private function restore_html_formatting($translated_text, $original_text) {
        // Ensure inputs are strings
        if (!is_string($translated_text) || !is_string($original_text)) {
            return is_string($translated_text) ? $translated_text : '';
        }
        
        AITRFOEL_Logger::log("restore_html_formatting called", 'debug');
    AITRFOEL_Logger::log("Original text: " . substr((string)$original_text, 0, 200) . (strlen((string)$original_text) > 200 ? '...' : ''), 'debug');
    AITRFOEL_Logger::log("Translated text: " . substr((string)$translated_text, 0, 200) . (strlen((string)$translated_text) > 200 ? '...' : ''), 'debug');
        
        // If no HTML in original, strip any HTML the AI added and return
        if ($original_text === wp_strip_all_tags($original_text)) {
            $cleaned = wp_strip_all_tags($translated_text);
            // Restore BR markers even in "no HTML" path (edge case: original was already cleaned)
            $cleaned = str_replace('≪BR≫', '<br>', $cleaned);
            if ($cleaned !== $translated_text) {
                AITRFOEL_Logger::log("No HTML in original, stripping AI-added HTML", 'debug');
            }
            AITRFOEL_Logger::log("No HTML in original text, returning cleaned translation", 'debug');
            return $cleaned;
        }
        
        // Original HAS HTML. Check if AI preserved the block-level structure.
        // Extract block-level tag sequence from original and translated.
        $block_tags = 'p|h[1-6]|ul|ol|li|div|blockquote|table|tr|td|th|thead|tbody|section|article|header|footer|figure|figcaption|pre';
        preg_match_all('/<(\/?)(' . $block_tags . ')[\s>]/i', $original_text, $orig_blocks);
        preg_match_all('/<(\/?)(' . $block_tags . ')[\s>]/i', $translated_text, $tr_blocks);
        
        $orig_sequence = implode(',', array_map(function($close, $tag) {
            return ($close ? '/' : '') . strtolower($tag);
        }, $orig_blocks[1], $orig_blocks[2]));
        
        $tr_sequence = implode(',', array_map(function($close, $tag) {
            return ($close ? '/' : '') . strtolower($tag);
        }, $tr_blocks[1], $tr_blocks[2]));
        
        if (!empty($orig_sequence) && $orig_sequence === $tr_sequence) {
            // AI preserved the same block-level HTML structure — use AI's response directly
            AITRFOEL_Logger::log("AI preserved block-level HTML structure, using AI response directly", 'info');
            // Still restore BR markers in case they survived in the AI response
            return str_replace('≪BR≫', '<br>', $translated_text);
        }
        
        // Block structure doesn't match — fall back to stripping and restoring
        $cleaned_translated = wp_strip_all_tags($translated_text);
        if ($cleaned_translated !== $translated_text) {
            AITRFOEL_Logger::log("WARNING: AI changed HTML structure, stripping it", 'warning');
            AITRFOEL_Logger::log("Cleaned response: " . substr($cleaned_translated, 0, 200), 'debug');
        }
        
        // If we have stored HTML placeholders, restore them
        $original_hash = md5($original_text);
        AITRFOEL_Logger::log("Looking for placeholders with hash: $original_hash", 'debug');
        AITRFOEL_Logger::log("Available hashes: " . implode(', ', array_keys($this->html_placeholders_storage)), 'debug');
        
        if (!empty($this->html_placeholders_storage[$original_hash])) {
            AITRFOEL_Logger::log("Using placeholder restoration with " . count($this->html_placeholders_storage[$original_hash]) . " placeholders for hash: $original_hash", 'debug');
            $result = $this->restore_html_with_placeholders($cleaned_translated, $this->html_placeholders_storage[$original_hash]);
            
            // Check if placeholder restoration actually worked
            // If the result is the same as input, placeholder restoration didn't work because
            // the AI response doesn't contain the placeholder markers
            if ($result === $cleaned_translated) {
                AITRFOEL_Logger::log("Placeholder restoration had no effect (AI didn't return placeholders), falling back to pattern matching", 'warning');
                $result = $this->restore_html_simple_patterns($cleaned_translated, $original_text);
                $result = str_replace('≪BR≫', '<br>', $result);
                return $result;
            }
            
            // Restore BR markers from placeholder path
            $result = str_replace('≪BR≫', '<br>', $result);
            return $result;
        } else {
            AITRFOEL_Logger::log("No placeholders found for hash: $original_hash", 'warning');
        }
        
        // Fallback to simple pattern matching for backward compatibility
        AITRFOEL_Logger::log("Using fallback pattern matching", 'debug');
        $result = $this->restore_html_simple_patterns($cleaned_translated, $original_text);
        
        // Always restore BR markers regardless of which path was taken
        $result = str_replace('≪BR≫', '<br>', $result);
        return $result;
    }
    
    /**
     * Restore HTML using stored placeholders
     */
    private function restore_html_with_placeholders($translated_text, $html_placeholders) {
        AITRFOEL_Logger::log("Restoring HTML with placeholders. Text length: " . strlen($translated_text), 'debug');
        AITRFOEL_Logger::log("Available placeholders: " . count($html_placeholders), 'debug');
        
        // First restore complex HTML elements (BEFORE BR tag replacement)
        foreach ($html_placeholders as $placeholder => $html_info) {
            $start_marker = '≪' . $placeholder . '≫';
            $end_marker = '≪/' . $placeholder . '≫';
            
            AITRFOEL_Logger::log("Looking for placeholder: $placeholder", 'debug');
            AITRFOEL_Logger::log("Start marker: $start_marker", 'debug');
            AITRFOEL_Logger::log("End marker: $end_marker", 'debug');
            
            // Check if markers exist in the text
            if (strpos($translated_text, $start_marker) !== false && strpos($translated_text, $end_marker) !== false) {
                AITRFOEL_Logger::log("Found markers for placeholder: $placeholder", 'debug');
                
                // Find content between markers - handle inline vs block elements differently
                $is_inline = in_array($html_info['tag'], ['strong', 'b', 'em', 'i', 'a', 'span']);
                
                if ($is_inline) {
                    // For inline elements, preserve space around markers
                    $pattern = '/\s*' . preg_quote($start_marker, '/') . '\s+(.*?)\s+' . preg_quote($end_marker, '/') . '\s*/s';
                } else {
                    // For block elements, use the original pattern
                    $pattern = '/\s*' . preg_quote($start_marker, '/') . '\s*(.*?)\s*' . preg_quote($end_marker, '/') . '\s*/s';
                }
                
                $translated_text = preg_replace_callback($pattern, function($matches) use ($html_info, $placeholder, $is_inline) {
                    $translated_content = trim($matches[1]);
                    $tag = $html_info['tag'];
                    $attributes = $html_info['attributes'];
                    
                    AITRFOEL_Logger::log("Restoring placeholder $placeholder: <$tag$attributes>$translated_content</$tag>", 'debug');
                    
                    $restored = "<{$tag}{$attributes}>{$translated_content}</{$tag}>";
                    
                    // For inline elements, add a space after to prevent concatenation
                    if ($is_inline) {
                        return $restored . ' ';
                    }
                    
                    return $restored;
                }, $translated_text);
            } else {
                AITRFOEL_Logger::log("Markers not found for placeholder: $placeholder", 'warning');
            }
        }
        
        // NOTE: BR marker restoration moved to restore_html_formatting() to cover all paths
        
        AITRFOEL_Logger::log("HTML restoration complete", 'debug');
        return $translated_text;
    }
    
    /**
     * Simple pattern matching for HTML restoration (fallback)
     */
    private function restore_html_simple_patterns($translated_text, $original_text) {
        AITRFOEL_Logger::log("Fallback restoration - Original: " . substr($original_text, 0, 200), 'debug');
        AITRFOEL_Logger::log("Fallback restoration - Translated: " . substr($translated_text, 0, 200), 'debug');
        
        // Strip any HTML that might have been added by the AI translation service
        $clean_translated = wp_strip_all_tags($translated_text);
        
        // STEP 1: Try to restore inline HTML elements using text matching
        $restored_with_inline = $this->restore_inline_html_fallback($clean_translated, $original_text);
        
        AITRFOEL_Logger::log("After inline HTML restoration: " . substr($restored_with_inline, 0, 200), 'debug');
        
        // STEP 2: Handle structural restoration
        $trimmed_original = trim($original_text);
        
        // Count the number of top-level HTML elements in the original
        $element_count = preg_match_all('/<([a-zA-Z0-9]+)(?:[^>]*)>.*?<\/\1>/s', $trimmed_original, $matches);
        
        AITRFOEL_Logger::log("Fallback: Found $element_count top-level HTML elements", 'debug');
        
        // If there's only ONE top-level element, we can safely restore it
        if ($element_count == 1) {
            // Handle nested elements FIRST (like <h3><strong>Title</strong></h3>)
            if (preg_match('/^<(h[1-6])([^>]*)><(strong|b|em|i)([^>]*)>(.*?)<\/\3><\/\1>$/s', $trimmed_original, $matches)) {
                $outer_tag = $matches[1];
                $outer_attrs = $matches[2];
                $inner_tag = $matches[3];
                $inner_attrs = $matches[4];
                $content = $matches[5];
                
                $result = "<{$outer_tag}{$outer_attrs}><{$inner_tag}{$inner_attrs}>{$restored_with_inline}</{$inner_tag}></{$outer_tag}>";
                AITRFOEL_Logger::log("Fallback: Restored nested element: $result", 'debug');
                return $result;
            }
            
            // Handle simple single element cases
            if (preg_match('/^<([a-zA-Z0-9]+)([^>]*)>(.*?)<\/\1>$/s', $trimmed_original, $matches)) {
                $tag = $matches[1];
                $attrs = $matches[2];
                $content = $matches[3];
                
                $result = "<{$tag}{$attrs}>{$restored_with_inline}</{$tag}>";
                AITRFOEL_Logger::log("Fallback: Restored single element <$tag>: $result", 'debug');
                return $result;
            }
        }
        
        // If we have MULTIPLE elements or complex structure, DO NOT attempt automatic splitting
        // This prevents the over-splitting issue you're experiencing
        if ($element_count > 1) {
            AITRFOEL_Logger::log("Fallback: Multiple elements detected - returning original structure with translated text inserted in first element only", 'warning');
            
            // Find the first paragraph or main content element and replace its content
            $result = preg_replace_callback('/<(p|h[1-6]|div)([^>]*)>(.*?)<\/\1>/s', function($matches) use ($restored_with_inline) {
                $tag = $matches[1];
                $attrs = $matches[2];
                $original_content = $matches[3];
                
                // Only replace the first match, preserve the rest as-is
                static $first_replacement = true;
                if ($first_replacement) {
                    $first_replacement = false;
                    AITRFOEL_Logger::log("Fallback: Replacing content in first <$tag> element", 'debug');
                    return "<{$tag}{$attrs}>{$restored_with_inline}</{$tag}>";
                }
                
                // Keep subsequent elements unchanged
                return $matches[0];
            }, $trimmed_original, 1); // Limit to 1 replacement
            
            if ($result !== $trimmed_original) {
                AITRFOEL_Logger::log("Fallback: Partial restoration completed", 'debug');
                return $result;
            }
        }
        
        // Last resort: return the translated text with any inline HTML we managed to restore
        AITRFOEL_Logger::log("Fallback: Could not restore HTML structure - returning text with inline HTML", 'warning');
        return $restored_with_inline;
    }
    
    /**
     * Try to restore inline HTML elements by matching text content with flexible patterns
     */
    private function restore_inline_html_fallback($translated_text, $original_text) {
        // Strategy 1: Extract all inline HTML elements and their text content
        $inline_patterns = [
            '/<strong[^>]*>(.*?)<\/strong>/i' => 'strong',
            '/<em[^>]*>(.*?)<\/em>/i' => 'em',  
            '/<a [^>]*href[^>]*>(.*?)<\/a>/i' => 'a',
            '/<span[^>]*>(.*?)<\/span>/i' => 'span',
            '/<b[^>]*>(.*?)<\/b>/i' => 'b',
            '/<i[^>]*>(.*?)<\/i>/i' => 'i'
        ];
        
        $found_elements = [];
        foreach ($inline_patterns as $pattern => $tag_type) {
            if (preg_match_all($pattern, $original_text, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $full_tag = $match[0];
                    $text_content = trim(wp_strip_all_tags($match[1]));
                    
                    if (!empty($text_content)) {
                        $found_elements[] = [
                            'full_tag' => $full_tag,
                            'text' => $text_content,
                            'type' => $tag_type
                        ];
                        
                        AITRFOEL_Logger::log("Found $tag_type: '" . $text_content . "' in " . substr($full_tag, 0, 50), 'debug');
                    }
                }
            }
        }
        
        AITRFOEL_Logger::log("Found " . count($found_elements) . " inline HTML elements for flexible restoration", 'debug');
        
        $restored = $translated_text;
        $restorations = 0;
        
        // Strategy 2: Flexible pattern matching for different element types
        foreach ($found_elements as $element) {
            $original_text_clean = $element['text'];
            $tag_type = $element['type'];
            $full_original_tag = $element['full_tag'];
            
            if ($tag_type === 'strong' || $tag_type === 'b') {
                // Look for numeric patterns in strong tags
                if (preg_match('/(\d+(?:[.,]\d+)*)\s*(million|thousand|individuals|foreigners|%)/i', $original_text_clean, $orig_matches)) {
                    // Try multiple patterns for different languages
                    $patterns_to_try = [
                        '/(\d+[.,]?\d*)\s*%/',  // Percentage
                        '/(\d+[.,\s]?\d*)\s*(milion[ůy]?|tisíc)/ui', // Million/thousand in Czech
                        '/(\d+[.,\s]?\d*)\s*(jednotlivců|osob|lidí)/ui', // Individuals in Czech
                        '/(\d+[.,\s]?\d*)\s*(cizinců|cizích)/ui', // Foreigners in Czech
                    ];
                    
                    foreach ($patterns_to_try as $pattern) {
                        if (preg_match($pattern, $restored, $trans_matches)) {
                            $translated_match = trim($trans_matches[0]);
                            
                            // Check if it's not already wrapped
                            if (strpos($restored, "<$tag_type>$translated_match</$tag_type>") === false) {
                                $restored = str_replace($translated_match, "<$tag_type>$translated_match</$tag_type>", $restored);
                                AITRFOEL_Logger::log("Restored $tag_type around: '$translated_match'", 'debug');
                                $restorations++;
                                break; // Stop after first successful match
                            }
                        }
                    }
                }
            } else if ($tag_type === 'a') {
                // For links, look for common link text patterns
                $link_patterns = [
                    '/(číst více|čtěte více)/ui',
                    '/(read more)/i',
                    '/(více informací|více info)/ui',
                    '/(pokračovat|continue)/ui',
                    '/(zjistit více|learn more)/ui'
                ];
                
                foreach ($link_patterns as $pattern) {
                    if (preg_match($pattern, $restored, $trans_matches)) {
                        $translated_link_text = $trans_matches[0];
                        
                        // Extract href from original
                        if (preg_match('/href=(["\'])([^"\']+)\1/i', $full_original_tag, $href_match)) {
                            $href = $href_match[2];
                            $new_link = "<a href=\"$href\">$translated_link_text</a>";
                            
                            // Only replace if not already a link
                            if (strpos($restored, $new_link) === false) {
                                $restored = str_replace($translated_link_text, $new_link, $restored);
                                AITRFOEL_Logger::log("Restored link: '$translated_link_text'", 'debug');
                                $restorations++;
                                break;
                            }
                        }
                    }
                }
            }
        }
        
        AITRFOEL_Logger::log("Total flexible restorations made: $restorations", 'debug');
        return $restored;
    }

    /**
     * Normalize a language value (name or code) to multilingual plugin language code.
     */
    private function normalize_lang_code($lang) {
        $code = $lang;
        if (is_string($lang) && strlen($lang) > 2 && class_exists('AITRFOEL_Multilingual_Manager')) {
            $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
            $mapped = $multilingual->get_language_code($lang);
            if ($mapped) { $code = $mapped; }
        }
        return $code;
    }

    /**
     * Determine if a URL is internal to this site.
     */
    private function is_internal_url($url) {
        if (!is_string($url) || $url === '') return false;
        // Skip special schemes
        if (preg_match('~^(#|mailto:|tel:|javascript:)~i', $url)) return false;
        $home = home_url('/');
        $home_parts = wp_parse_url($home);
        $u = wp_parse_url($url);
        // Relative URL => internal
        if ($u === false || !isset($u['host'])) return true;
        
        $is_internal = isset($home_parts['host']) && strcasecmp($home_parts['host'], $u['host']) === 0;
        
        // Support staging/production domain mismatch during local development
        if (!$is_internal && isset($home_parts['host'])) {
            $path = isset($u['path']) ? $u['path'] : '';
            if ($path !== '' && $path !== '/' && !preg_match('/\\.(jpg|jpeg|png|gif|css|js|pdf|zip|svg)$/i', $path)) {
                $local_url = str_replace($u['host'], $home_parts['host'], $url);
                if (url_to_postid($local_url) > 0) {
                    return true;
                }
            }
        }
        
        return $is_internal;
    }

    /**
     * Build absolute URL for a (possibly relative) href.
     */
    private function to_absolute_url($href) {
        if (!is_string($href) || $href === '') return $href;
        $u = wp_parse_url($href);
        if ($u === false) return $href;
        if (isset($u['scheme']) && isset($u['host'])) return $href; // already absolute

        $path = isset($u['path']) ? $u['path'] : '';
        if (strpos($path, '/') !== 0) {
            // Relative path - resolve relative to home_url (simplistic fallback)
            $path = '/' . $path;
        }

        return home_url($path . (isset($u['query']) ? ('?' . $u['query']) : '') . (isset($u['fragment']) ? ('#' . $u['fragment']) : ''));
    }

    /**
     * Localize a single URL to the target language using Polylang mappings when possible.
     */
    private function localize_single_url($href, $target_lang) {
        if (!is_string($href) || $href === '') return $href;
        if (!$this->is_internal_url($href)) return $href;

        $target_code = $this->normalize_lang_code($target_lang);
        $abs = $this->to_absolute_url($href);

        // Separate query/fragment
        $parts = wp_parse_url($abs);
        if ($parts === false) return $href;
        $base_no_qf = (isset($parts['scheme']) ? $parts['scheme'] . '://' : '') .
                      (isset($parts['host']) ? $parts['host'] : '') .
                      (isset($parts['port']) ? (':' . $parts['port']) : '') .
                      (isset($parts['path']) ? $parts['path'] : '');
        $suffix = (isset($parts['query']) ? ('?' . $parts['query']) : '') . (isset($parts['fragment']) ? ('#' . $parts['fragment']) : '');

        // 1) Best-effort: map post ID to its translation
        $post_id = url_to_postid($base_no_qf);
        if (!$post_id && isset($parts['host'])) {
            $local_host = wp_parse_url(home_url(), PHP_URL_HOST);
            if ($local_host && strcasecmp($parts['host'], $local_host) !== 0) {
                $local_base = str_replace($parts['host'], $local_host, $base_no_qf);
                $post_id = url_to_postid($local_base);
            }
        }

        // Fallback: Resolve by slug (last path segment) if url_to_postid failed.
        // This handles shortened URLs, legacy/redirect links, or nested pages
        // whose static links in the database don't reflect the full current nested path.
        if (!$post_id && isset($parts['path'])) {
            $path_trimmed = trim($parts['path'], '/');
            if ($path_trimmed !== '') {
                $path_segments = explode('/', $path_trimmed);
                $slug = end($path_segments);
                if ($slug !== '') {
                    global $wpdb;
                    $pt_allowed = ['page', 'post', 'product'];
                    $pt_allowed = apply_filters('aitrfoel_link_resolution_post_types', $pt_allowed);
                    $in_clause = "'" . implode("','", array_map('esc_sql', $pt_allowed)) . "'";
                    
                    $resolved_id = (int) $wpdb->get_var($wpdb->prepare(
                        "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type IN ($in_clause) AND post_status = 'publish' LIMIT 1",
                        $slug
                    ));
                    if ($resolved_id > 0) {
                        $post_id = $resolved_id;
                    }
                }
            }
        }

        if ($post_id) {
            $multilingual = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null;
            if ($multilingual && $multilingual->is_active()) {
                $translations = $multilingual->get_post_translations($post_id);
                if (isset($translations[$target_code])) {
                    $translated_id = (int) $translations[$target_code];
                    if ($translated_id > 0) {
                        $new = get_permalink($translated_id);
                        if (is_string($new) && $new !== '') {
                            // Restore original host/scheme if mismatch (local dev / production database sync support)
                            if (isset($parts['host'])) {
                                $new_parts = wp_parse_url($new);
                                if ($new_parts && isset($new_parts['host'])) {
                                    $new = str_replace($new_parts['host'], $parts['host'], $new);
                                    if (isset($parts['scheme']) && isset($new_parts['scheme'])) {
                                        $new = str_replace($new_parts['scheme'] . '://', $parts['scheme'] . '://', $new);
                                    }
                                }
                            }
                            return $new . $suffix;
                        }
                    }
                }
            } elseif (function_exists('pll_get_post') && $target_code) {
                $translated_id = pll_get_post($post_id, $target_code);
                if ($translated_id) {
                    $new = get_permalink($translated_id);
                    if (is_string($new) && $new !== '') {
                        return $new . $suffix;
                    }
                }
            }
        }

        // 2) Fallback: swap language home base, keep path
        // Use multilingual manager if available, fallback to Polylang functions for backward compatibility
        $multilingual = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null;
        $default_lang = $multilingual ? $multilingual->get_default_language() : (function_exists('pll_default_language') ? pll_default_language() : null);
        
        // Get home URLs - Polylang has pll_home_url, WPML uses filters
        if (function_exists('pll_home_url')) {
            $home_default = pll_home_url($default_lang) ?: home_url('/');
            $home_target = pll_home_url($target_code) ?: home_url('/');
        } elseif (function_exists('icl_get_home_url')) {
            // WPML home URL function
            $home_default = icl_get_home_url() ?: home_url('/');
            $home_target = $target_code ? apply_filters('wpml_home_url', home_url('/'), $target_code) : home_url('/');
        } else {
            $home_default = home_url('/');
            $home_target = home_url('/');
        }

        // Normalize trailing slashes
        $home_default = trailingslashit($home_default);
        $home_target = trailingslashit($home_target);

        if (stripos($abs, $home_default) === 0) {
            $tail = substr($abs, strlen($home_default));
            return $home_target . $tail; // suffix already included in $abs
        }
        // Also try plain home_url as base
        $home_plain = trailingslashit(home_url('/'));
        if (stripos($abs, $home_plain) === 0) {
            $tail = substr($abs, strlen($home_plain));
            return $home_target . $tail;
        }

        return $href; // unchanged
    }

    /**
     * Rewrite href attributes inside an HTML blob to the target language.
     */
    public function localize_hrefs_in_html($html, $target_lang) {
        if (!is_string($html) || stripos($html, '<a') === false || stripos($html, 'href=') === false) return $html;
        $self = $this;
        $cb = function($m) use ($self, $target_lang) {
            $full_tag = $m[0];
            $q = $m[1];
            $href = $m[2];
            $new = $self->localize_single_url($href, $target_lang);
            if ($new === $href) return $full_tag;
            // Replace only the first href occurrence within this tag
            $replaced = preg_replace('/href=([\"\'])[^\"\']*\1/i', 'href=' . $q . esc_url($new) . $q, $full_tag, 1);
            return is_string($replaced) ? $replaced : $full_tag;
        };
        return preg_replace_callback('/<a\s[^>]*href=([\"\'])([^\"\']+)\1[^>]*>/i', $cb, $html);
    }

    /**
     * Swap nav menu IDs in Elementor data to point to translated menus.
     * Handles Elementor Nav Menu widgets which store 'menu' as a term_id in settings.
     *
     * @param array  &$data       Elementor data array (modified in place).
     * @param array  $menu_id_map Map of source_menu_id => [lang => translated_menu_id].
     * @param string $target_lang Target language code.
     * @return int Number of menu IDs swapped.
     */
    private function swap_menu_ids_in_elementor_data(&$data, $menu_id_map, $target_lang) {
        $swapped = 0;
        foreach ($data as &$element) {
            if (!is_array($element)) continue;

            // Check if this is a nav-menu widget
            $widget_type = isset($element['widgetType']) ? $element['widgetType'] : '';
            if ($widget_type === 'nav-menu' && isset($element['settings']['menu'])) {
                $source_menu_id = (int) $element['settings']['menu'];
                if ($source_menu_id > 0 && isset($menu_id_map[$source_menu_id][$target_lang])) {
                    $element['settings']['menu'] = (string) $menu_id_map[$source_menu_id][$target_lang];
                    $swapped++;
                }
            }

            // Recurse into child elements
            if (isset($element['elements']) && is_array($element['elements'])) {
                $swapped += $this->swap_menu_ids_in_elementor_data($element['elements'], $menu_id_map, $target_lang);
            }
        }
        unset($element);
        return $swapped;
    }

    /**
     * Swap nested Elementor template references to translated template IDs.
     *
     * Covers common reusable-element patterns such as the Elementor Template widget
     * (`settings.template_id`) and Global Widget references (`templateID`).
     *
     * @param array  &$data       Elementor data array (modified in place).
     * @param string $target_lang Target language code.
     * @return int Number of template references swapped.
     */
    private function swap_elementor_template_refs_in_data(&$data, $target_lang) {
        $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
        if (!$multilingual->is_active()) {
            return 0;
        }

        $swapped = 0;
        foreach ($data as &$element) {
            if (!is_array($element)) continue;

            $widget_type = isset($element['widgetType']) ? $element['widgetType'] : '';

            if ($widget_type === 'template' && isset($element['settings']['template_id'])) {
                $source_template_id = (int) $element['settings']['template_id'];
                $translated_template_id = $this->get_translated_elementor_template_id($source_template_id, $target_lang, $multilingual);
                if ($translated_template_id > 0) {
                    $element['settings']['template_id'] = (string) $translated_template_id;
                    $swapped++;
                }
            }

            if (isset($element['templateID'])) {
                $source_template_id = (int) $element['templateID'];
                $translated_template_id = $this->get_translated_elementor_template_id($source_template_id, $target_lang, $multilingual);
                if ($translated_template_id > 0) {
                    $element['templateID'] = $translated_template_id;
                    $swapped++;
                }
            }

            if (isset($element['elements']) && is_array($element['elements'])) {
                $swapped += $this->swap_elementor_template_refs_in_data($element['elements'], $target_lang);
            }
        }
        unset($element);

        return $swapped;
    }

    /**
     * Resolve the translated Elementor template ID for a target language.
     *
     * @param int                           $template_id  Source template ID.
     * @param string                        $target_lang  Target language code.
     * @param AITRFOEL_Multilingual_Manager $multilingual Multilingual manager instance.
     * @return int
     */
    private function get_translated_elementor_template_id($template_id, $target_lang, $multilingual) {
        if ($template_id <= 0 || get_post_type($template_id) !== 'elementor_library') {
            return 0;
        }

        $translations = $multilingual->get_post_translations($template_id);
        if (!isset($translations[$target_lang])) {
            return 0;
        }

        $translated_template_id = (int) $translations[$target_lang];
        if ($translated_template_id <= 0 || get_post_status($translated_template_id) !== 'publish') {
            return 0;
        }

        return $translated_template_id;
    }

    /**
     * Walk Elementor translated data and localize internal links for target language.
     */
    public function localize_internal_links(&$data, $target_lang) {
        $walker = function (&$node, $keyPath = []) use (&$walker, $target_lang) {
            if (is_array($node)) {
                // Special-case: Elementor setting 'link' => ['url' => ...]
                if (isset($node['url']) && is_string($node['url'])) {
                    // Heuristic: if parent path ends with '.settings.link' or contains '.link.' treat as link url
                    $pathStr = implode('.', $keyPath);
                    if (preg_match('~(^|\.)link(\.|$)~', $pathStr)) {
                        $new = $this->localize_single_url($node['url'], $target_lang);
                        if ($new !== $node['url']) {
                            $node['url'] = $new;
                        }
                    }
                }
                foreach ($node as $k => &$v) {
                    $walker($v, array_merge($keyPath, [$k]));
                }
                unset($v);
                return;
            }
            if (is_string($node)) {
                // Only touch strings that look like HTML with anchors
                if (stripos($node, '<a') !== false && stripos($node, 'href=') !== false) {
                    $newHtml = $this->localize_hrefs_in_html($node, $target_lang);
                    if (is_string($newHtml) && $newHtml !== $node) {
                        $node = $newHtml;
                    }
                }
                return;
            }
        };
        $walker($data, []);
    }

    /**
     * Rewrite [elementor-template id="X"] shortcodes inside post_content so each
     * id points at the current-language translation of template X.
     *
     * Safe to call on any string: only the `id` attribute of `elementor-template`
     * shortcodes is touched, and only when a published translation exists in
     * $target_lang. Source-language posts and non-translated templates are left
     * untouched. The runtime pre_do_shortcode_tag swap added in 1.7.07 acts as
     * a fallback for posts that still carry source ids.
     *
     * @param string $content     Post content (may contain shortcodes).
     * @param string $target_lang Target language code (e.g. 'de').
     * @return string Rewritten content.
     */
    public function rewrite_elementor_template_shortcode_ids($content, $target_lang) {
        if (!is_string($content) || $content === '' || stripos($content, '[elementor-template') === false) {
            return $content;
        }
        $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
        if (!$multilingual || !$multilingual->is_active()) {
            return $content;
        }

        return preg_replace_callback(
            '/\[elementor-template\b([^\]]*)\]/i',
            function ($m) use ($multilingual, $target_lang) {
                $attrs = $m[1];
                if (!preg_match('/\bid\s*=\s*(["\']?)(\d+)\1/i', $attrs, $idm)) {
                    return $m[0];
                }
                $orig_id = (int) $idm[2];
                if ($orig_id <= 0 || get_post_type($orig_id) !== 'elementor_library') {
                    return $m[0];
                }
                $translations = $multilingual->get_post_translations($orig_id);
                if (!isset($translations[$target_lang])) {
                    return $m[0];
                }
                $new_id = (int) $translations[$target_lang];
                if ($new_id <= 0 || $new_id === $orig_id || get_post_status($new_id) !== 'publish') {
                    return $m[0];
                }
                $new_attrs = preg_replace(
                    '/\bid\s*=\s*(["\']?)(\d+)\1/i',
                    'id=' . ($idm[1] !== '' ? $idm[1] : '"') . $new_id . ($idm[1] !== '' ? $idm[1] : '"'),
                    $attrs,
                    1
                );
                return '[elementor-template' . $new_attrs . ']';
            },
            $content
        );
    }

    /**
     * Split translated content into parts matching the number of original elements
     */
    private function split_translated_content($translated_text, $expected_parts) {
        AITRFOEL_Logger::log("Splitting translated content into $expected_parts parts", 'debug');
        
        // Try different splitting strategies
        
        // Strategy 1: Split by double newlines (paragraph breaks)
        $parts = preg_split('/\n\s*\n/', trim($translated_text));
        $parts = array_filter(array_map('trim', $parts)); // Remove empty parts
        
        if (count($parts) == $expected_parts) {
            AITRFOEL_Logger::log("Split by paragraph breaks successful", 'debug');
            return array_values($parts);
        }
        
        // Strategy 2: Split by single newlines
        if (count($parts) < $expected_parts) {
            $parts = preg_split('/\n/', trim($translated_text));
            $parts = array_filter(array_map('trim', $parts));
            
            if (count($parts) == $expected_parts) {
                AITRFOEL_Logger::log("Split by single newlines successful", 'debug');
                return array_values($parts);
            }
        }
        
        // Strategy 3: Split by sentences (periods followed by space/newline)
        if (count($parts) < $expected_parts) {
            $parts = preg_split('/\.\s*(?=[A-Z])|\n/', trim($translated_text));
            $parts = array_filter(array_map(function($part) {
                $part = trim($part);
                // Re-add period if it was removed by splitting
                if (!empty($part) && !preg_match('/[.!?]$/', $part)) {
                    $part .= '.';
                }
                return $part;
            }, $parts));
            
            if (count($parts) >= $expected_parts) {
                AITRFOEL_Logger::log("Split by sentences successful", 'debug');
                return array_slice(array_values($parts), 0, $expected_parts);
            }
        }
        
        // Strategy 4: If we have more parts than expected, try to merge some
        if (count($parts) > $expected_parts) {
            $merged_parts = [];
            $parts_per_expected = ceil(count($parts) / $expected_parts);
            
            for ($i = 0; $i < $expected_parts; $i++) {
                $start = $i * $parts_per_expected;
                $end = min(($i + 1) * $parts_per_expected, count($parts));
                $merged_parts[] = implode(' ', array_slice($parts, $start, $end - $start));
            }
            
            AITRFOEL_Logger::log("Merged parts strategy used", 'debug');
            return $merged_parts;
        }
        
        // Fallback: Just return the whole text as a single part for the first element
        AITRFOEL_Logger::log("Using fallback strategy - single part", 'debug');
        return [$translated_text];
    }
    
    /**
     * Create a new page with translated content
     * 
     * @param int $original_page_id Original page ID
     * @param array $translated_data Translated Elementor data
     * @param string $target_lang Target language
     * @param string $detected_lang Detected source language
     * @return int|WP_Error New page ID or error
     */
    private function create_translated_page($original_page_id, $translated_data, $target_lang, $detected_lang = 'en', $pre_translated_title = null, $is_partial = false) {
        $original_page = get_post($original_page_id);
        
        if (!$original_page) {
            return new WP_Error('page_not_found', __('Original page not found.', 'ai-translator-for-elementor-and-polylang'));
        }
        
        // Check if a translation already exists for this language — update it instead of creating a duplicate
        $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
        $existing_translations = $multilingual->is_active() ? $multilingual->get_post_translations($original_page_id) : [];
        
        // Normalize target_lang to code for lookup (translations are keyed by code)
        $target_code = $target_lang;
        if (strlen($target_lang) > 3 && class_exists('AITRFOEL_Language_Mapper')) {
            $resolved = AITRFOEL_Language_Mapper::get_code_from_language_name($target_lang);
            if ($resolved) {
                $target_code = $resolved;
            }
        }
        $existing_page_id = isset($existing_translations[$target_code]) ? (int) $existing_translations[$target_code] : 0;

        // Fallback: if the multilingual plugin didn't register the relationship (e.g. the
        // first pass was a partial save that returned before Polylang committed the link),
        // locate the previously created page via our own tracking meta.
        if (!$existing_page_id || !get_post($existing_page_id)) {
            $fallback_posts = get_posts([
                'post_type'              => $original_page->post_type,
                'post_status'            => ['publish', 'draft', 'private', 'pending'],
                'posts_per_page'         => 1,
                'fields'                 => 'ids',
                'no_found_rows'          => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false,
                'meta_query'             => [[
                    'key'     => '_aitrfoel_translated_from',
                    'value'   => $original_page_id . '_' . $target_code,
                    'compare' => '=',
                ]],
            ]);
            if (!empty($fallback_posts)) {
                $existing_page_id = (int) $fallback_posts[0];
                AITRFOEL_Logger::log(
                    sprintf('Fallback lookup: found existing translation (ID %d) via tracking meta for page %d → %s.',
                        $existing_page_id, $original_page_id, $target_lang),
                    'info',
                    $original_page_id
                );
            }
        }

        if ($existing_page_id && get_post($existing_page_id)) {
            AITRFOEL_Logger::log(
                sprintf('Existing translation found (ID %d) for page %d → %s. Updating instead of creating new.', $existing_page_id, $original_page_id, $target_lang),
                'info',
                $original_page_id
            );
            
            // Update existing translated page's Elementor data
            update_post_meta($existing_page_id, '_elementor_data', wp_slash(json_encode($translated_data)));

            // Update existing page's post_content with localized links
            $content_local = $this->localize_hrefs_in_html($original_page->post_content, $target_lang);
            wp_update_post([
                'ID'           => $existing_page_id,
                'post_content' => $content_local,
            ]);

            // Translated templates must NEVER have display conditions — ensure they are removed
            // even if a previous plugin version accidentally copied them.
            if (get_post_type($original_page_id) === 'elementor_library') {
                delete_post_meta($existing_page_id, '_elementor_conditions');
                // Also strip conditions from _elementor_page_settings to prevent Elementor Pro
                // re-adding _elementor_conditions via its updated_post_meta hook.
                $page_settings = get_post_meta($existing_page_id, '_elementor_page_settings', true);
                if (is_array($page_settings) && isset($page_settings['conditions'])) {
                    unset($page_settings['conditions']);
                    update_post_meta($existing_page_id, '_elementor_page_settings', $page_settings);
                }
            }
            
            // Use pre-translated title if available (from batch), otherwise translate separately
            $translated_title = $pre_translated_title;
            if (!$translated_title && !$is_partial) {
                $api = AITRFOEL_OpenAI_API::get_instance();
                $translated_title = $api->translate_title($original_page->post_title, $target_lang, $detected_lang);
                if (is_wp_error($translated_title)) {
                    $translated_title = null;
                }
            }
            if ($translated_title) {
                $update_args = [
                    'ID' => $existing_page_id,
                    'post_title' => $translated_title,
                ];

                // Optionally update slug from translated title.
                if (get_option('aitrfoel_translate_url_slugs', true)) {
                    $translated_slug = sanitize_title($translated_title);
                    if (!empty($translated_slug)) {
                        $update_args['post_name'] = $translated_slug;
                    }
                }

                wp_update_post($update_args);
            }
            
            // Copy theme/layout meta from original to keep in sync
            $theme_meta_keys = [
                '_wp_page_template', 'site-post-title', 'ast-title-bar-display',
                'ast-featured-img', 'ast-site-content-layout', 'site-sidebar-layout',
                'ast-main-header-display', 'footer-sml-layout', 'theme-transparent-header-meta'
            ];
            foreach ($theme_meta_keys as $meta_key) {
                $meta_value = get_post_meta($original_page_id, $meta_key, true);
                if ($meta_value !== '') {
                    update_post_meta($existing_page_id, $meta_key, $meta_value);
                }
            }
            
            // Track word usage for updates too
            $word_count = 0;
            foreach ($translated_data as $element) {
                $word_count += $this->count_element_words($element);
            }
            if ($word_count > 0 && class_exists('AITRFOEL_License_Manager_Server')) {
                $license_manager = AITRFOEL_License_Manager_Server::get_instance();
                if (method_exists($license_manager, 'report_word_usage')) {
                    $license_manager->report_word_usage($word_count, $target_lang);
                }
            }
            
            // Translate SEO meta, excerpt, and WooCommerce fields (skip for partial saves)
            if (!$is_partial) {
                $this->translate_post_meta_fields($original_page_id, $existing_page_id, $target_lang, $detected_lang);
            }

            // Assign translated WooCommerce product categories/tags
            $this->translate_product_taxonomies($original_page_id, $existing_page_id, $target_lang);

            // Translate WooCommerce product attributes
            $this->translate_product_attributes($original_page_id, $existing_page_id, $target_lang, $detected_lang);

            // Translate product variations for variable products
            $this->translate_product_variations($original_page_id, $existing_page_id, $target_lang, $detected_lang);

            // Translate product gallery image alt text/captions
            $this->translate_product_gallery_meta($original_page_id, $existing_page_id, $target_lang, $detected_lang);

            // Translate and assign post categories/tags (non-WooCommerce)
            $this->translate_post_taxonomies($original_page_id, $existing_page_id, $target_lang);

            // Ensure the source-page meta is present (may be missing on older translations)
            update_post_meta($existing_page_id, '_aitrfoel_source_page', $original_page_id);

            AITRFOEL_Logger::log(
                sprintf('Successfully updated existing translation (ID %d)', $existing_page_id),
                'info',
                $original_page_id
            );
            
            return $existing_page_id;
        }
        
        // Use pre-translated title if available (from batch), otherwise translate separately
        $translated_title = $pre_translated_title;
        if (!$translated_title && !$is_partial) {
            $api = AITRFOEL_OpenAI_API::get_instance();
            $translated_title = $api->translate_title($original_page->post_title, $target_lang, $detected_lang);
        }
        
        if (is_wp_error($translated_title) || !$translated_title) {
            // Fallback: use original title with language code suffix
            $translated_title = $original_page->post_title . ' (' . strtoupper($target_lang) . ')';
        }
        
        // Create new page with intelligent status selection
        $target_status = get_option('aitrfoel_translated_page_status', 'draft');
        
        // If 'inherit', match the original post's status
        if ($target_status === 'inherit') {
            $target_status = ($original_page->post_status === 'publish') ? 'publish' : 'draft';
        }
        
        // Optionally generate a translated slug; otherwise preserve source-style slug.
        if (get_option('aitrfoel_translate_url_slugs', true)) {
            $translated_slug = sanitize_title($translated_title);
            if (empty($translated_slug)) {
                $translated_slug = $original_page->post_name . '-' . $target_lang;
            }
        } else {
            $translated_slug = $original_page->post_name . '-' . $target_lang;
        }
        
        $content_local = $this->localize_hrefs_in_html($original_page->post_content, $target_lang);
        
        $new_page_args = [
            'post_title' => $translated_title,
            'post_name' => $translated_slug,
            'post_content' => $content_local,
            'post_status' => $target_status,
            'post_type' => $original_page->post_type,
            'post_parent' => $this->get_translated_parent_id($original_page->post_parent, $target_lang),
            'menu_order' => $original_page->menu_order,
            'post_excerpt' => $original_page->post_excerpt,
            'post_date' => $original_page->post_date,
            'post_date_gmt' => $original_page->post_date_gmt,
        ];
        
        $new_page_id = wp_insert_post($new_page_args);
        
        if (is_wp_error($new_page_id)) {
            return $new_page_id;
        }

        // Store the source-page + language so we can find this page on partial-save retries
        // even when the multilingual plugin hasn't committed the translation relationship yet.
        update_post_meta($new_page_id, '_aitrfoel_translated_from', $original_page_id . '_' . $target_code);
        update_post_meta($new_page_id, '_aitrfoel_source_page', $original_page_id);

        // Reassert the requested status after insertion in case another hook/plugin
        // forced a different post_status during wp_insert_post().
        if (get_post_status($new_page_id) !== $target_status) {
            wp_update_post([
                'ID' => $new_page_id,
                'post_status' => $target_status,
            ]);
            clean_post_cache($new_page_id);
        }
        
        // Update Elementor data
    update_post_meta($new_page_id, '_elementor_data', wp_slash(json_encode($translated_data)));
        update_post_meta($new_page_id, '_elementor_edit_mode', 'builder');
        
        // Copy other Elementor meta
        $elementor_meta_keys = [
            '_elementor_template_type',
            '_elementor_version',
            '_elementor_pro_version',
            '_elementor_page_settings',
            '_elementor_controls_usage',
        ];
        
        // Copy Astra theme meta fields to preserve page layout settings
        $astra_meta_keys = [
            '_wp_page_template',             // The base page template (e.g., 'default')
            'site-post-title',               // Controls page title visibility ('disabled')
            'ast-title-bar-display',         // Disables the title bar area ('disabled')
            'ast-featured-img',              // Disables the featured image ('disabled')
            'ast-site-content-layout',       // **CRITICAL**: Sets the page layout (e.g., 'full-width-container')
            'site-sidebar-layout',           // **CRITICAL**: Sets the sidebar layout ('no-sidebar')
            'ast-main-header-display',       // Header display setting
            'footer-sml-layout',             // Footer layout
            'theme-transparent-header-meta'  // Transparent header setting
        ];
        
        // Combine all meta keys to copy
        $all_meta_keys = array_merge($elementor_meta_keys, $astra_meta_keys);
        
        $is_elementor_template = get_post_type($original_page_id) === 'elementor_library';

        foreach ($all_meta_keys as $meta_key) {
            $meta_value = get_post_meta($original_page_id, $meta_key, true);
            if ($meta_value !== '') { // Copy even if false/0, but not if completely empty
                // Strip display conditions from _elementor_page_settings for translated templates.
                // Elementor Pro reads the 'conditions' key inside this meta and re-writes
                // _elementor_conditions via an updated_post_meta hook, undoing our delete below.
                if ($is_elementor_template && $meta_key === '_elementor_page_settings' && is_array($meta_value)) {
                    unset($meta_value['conditions']);
                }
                update_post_meta($new_page_id, $meta_key, $meta_value);
            }
        }

        // Translated templates must NEVER have display conditions. Elementor's location resolver
        // uses conditions to build its candidates list — if translated templates have conditions
        // they compete with the original and cause wrong-language templates to appear.
        // Our filter_elementor_location_templates() injects translated templates into the
        // candidate list at runtime by swapping originals, so conditions are not needed.
        if ($is_elementor_template) {
            delete_post_meta($new_page_id, '_elementor_conditions');
        }
        
        // Handle multilingual plugin (Polylang or WPML) via abstraction layer
        $multilingual = AITRFOEL_Multilingual_Manager::get_instance();
        if ($multilingual->is_active()) {
            $plugin_name = $multilingual->get_plugin_display_name();
            AITRFOEL_Logger::log(
                sprintf('Calling %s integration for %d -> %d, target lang: %s', $plugin_name, $original_page_id, $new_page_id, $target_lang),
                'debug',
                $original_page_id
            );
            
            // Pass the 2-letter language code directly
            $result = $multilingual->setup_translation_relationship($original_page_id, $new_page_id, $target_lang);
            
            if (is_wp_error($result)) {
                AITRFOEL_Logger::log(
                    $plugin_name . ' setup FAILED: ' . $result->get_error_message() . ' (Code: ' . $result->get_error_code() . ')',
                    'error',
                    $original_page_id,
                    $new_page_id
                );
            } else {
                AITRFOEL_Logger::log(
                    sprintf('✓ %s integration successful for pages %d -> %d', $plugin_name, $original_page_id, $new_page_id),
                    'success',
                    $original_page_id,
                    $new_page_id
                );
            }
        } else {
            AITRFOEL_Logger::log('No multilingual plugin is active, skipping translation relationship setup', 'info', $original_page_id);
        }
        
        // Translate SEO meta, excerpt, and WooCommerce fields (skip for partial saves)
        if (!$is_partial) {
            $this->translate_post_meta_fields($original_page_id, $new_page_id, $target_lang, $detected_lang);
        }

        // Assign translated WooCommerce product categories/tags
        $this->translate_product_taxonomies($original_page_id, $new_page_id, $target_lang);

        // Translate WooCommerce product attributes
        $this->translate_product_attributes($original_page_id, $new_page_id, $target_lang, $detected_lang);

        // Translate product variations for variable products
        $this->translate_product_variations($original_page_id, $new_page_id, $target_lang, $detected_lang);

        // Translate product gallery image alt text/captions
        $this->translate_product_gallery_meta($original_page_id, $new_page_id, $target_lang, $detected_lang);

        // Translate and assign post categories/tags (non-WooCommerce)
        $this->translate_post_taxonomies($original_page_id, $new_page_id, $target_lang);

        // Clear Elementor cache
        if (class_exists('\Elementor\Plugin')) {
            \Elementor\Plugin::$instance->files_manager->clear_cache();
        }
        
        // Post-creation processing for proper LiteSpeed integration
        $this->post_creation_processing($new_page_id);
        
        return $new_page_id;
    }

    /**
     * Translate SEO metadata (Yoast SEO, Rank Math), post excerpt,
     * and copy WooCommerce product fields for a translated post.
     *
     * @param int    $original_id  Source post ID
     * @param int    $target_id    Translated post ID
     * @param string $target_lang  Target language code
     * @param string $source_lang  Source language code
     */
    /**
     * Whether JetEngine is active on this site.
     *
     * @return bool
     */
    private function is_jetengine_active() {
        return defined('JET_ENGINE_VERSION') || class_exists('Jet_Engine');
    }

    /**
     * Decode a JetEngine option that may be stored as a JSON string or a PHP array.
     *
     * @param mixed $val
     * @return array
     */
    private function jet_maybe_decode($val) {
        if (is_string($val)) {
            $decoded = json_decode($val, true);
            return (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
        }
        return is_array($val) ? $val : [];
    }

    /**
     * Flatten a JetEngine "meta_fields" definition array into [ meta_key => type ].
     *
     * @param mixed $meta_fields
     * @param array $out  Accumulator passed by reference.
     */
    private function jet_collect_fields($meta_fields, &$out) {
        if (!is_array($meta_fields)) {
            return;
        }
        foreach ($meta_fields as $field) {
            $field = (array) $field;
            $name  = isset($field['name']) ? trim((string) $field['name']) : '';
            if ($name === '') {
                continue;
            }
            $type = isset($field['type']) ? (string) $field['type'] : 'text';
            $out[$name] = $type;
        }
    }

    /**
     * Map of JetEngine custom fields registered for a post type: [ meta_key => type ].
     * Reads both JetEngine-registered CPTs (jet_post_types) and standalone meta boxes
     * (jet_engine_meta_boxes). Cached per request. Returns [] when JetEngine is inactive.
     *
     * @param string $post_type
     * @return array
     */
    private function get_jetengine_fields($post_type) {
        static $cache = [];
        if (isset($cache[$post_type])) {
            return $cache[$post_type];
        }

        $fields = [];
        if (!$this->is_jetengine_active() || $post_type === '') {
            return $cache[$post_type] = $fields;
        }

        // 1) Custom post types registered through JetEngine.
        $cpts = $this->jet_maybe_decode(get_option('jet_post_types', []));
        foreach ($cpts as $cpt) {
            $cpt  = (array) $cpt;
            $slug = isset($cpt['slug']) ? $cpt['slug'] : (isset($cpt['args']['slug']) ? $cpt['args']['slug'] : '');
            if ($slug !== $post_type) {
                continue;
            }
            $this->jet_collect_fields(isset($cpt['meta_fields']) ? $cpt['meta_fields'] : [], $fields);
        }

        // 2) Standalone JetEngine meta boxes attached to this post type.
        $boxes = $this->jet_maybe_decode(get_option('jet_engine_meta_boxes', []));
        foreach ($boxes as $box) {
            $box  = (array) $box;
            $args = isset($box['args']) ? (array) $box['args'] : [];
            $allowed = [];
            if (isset($args['allowed_post_type'])) {
                $allowed = (array) $args['allowed_post_type'];
            } elseif (isset($args['post_type'])) {
                $allowed = (array) $args['post_type'];
            }
            if (!in_array($post_type, $allowed, true)) {
                continue;
            }
            $this->jet_collect_fields(isset($box['meta_fields']) ? $box['meta_fields'] : [], $fields);
        }

        return $cache[$post_type] = $fields;
    }

    /**
     * Meta keys of JetEngine text-type fields (text / textarea / wysiwyg / html) for
     * a post type — the fields whose values should be translated.
     *
     * @param string $post_type
     * @return string[]
     */
    private function get_jetengine_text_meta_keys($post_type) {
        $text_types = ['text', 'textarea', 'wysiwyg', 'html'];
        $keys = [];
        foreach ($this->get_jetengine_fields($post_type) as $key => $type) {
            if (in_array($type, $text_types, true)) {
                $keys[] = $key;
            }
        }
        return $keys;
    }

    /**
     * Copy every JetEngine custom field value from the source post to the translation,
     * verbatim. Ensures non-text fields (media, dates, numbers, relations) are present
     * on the translation; text fields are translated afterwards by the caller. No-op
     * when JetEngine is inactive or the post type has no JetEngine fields.
     *
     * @param int $original_id
     * @param int $target_id
     */
    private function copy_jetengine_meta($original_id, $target_id) {
        $fields = $this->get_jetengine_fields(get_post_type($original_id));
        if (empty($fields)) {
            return;
        }
        $copied = 0;
        foreach ($fields as $key => $type) {
            $val = get_post_meta($original_id, $key, true);
            if ($val === '' || $val === null) {
                continue;
            }
            update_post_meta($target_id, $key, $val);
            $copied++;
        }
        if ($copied > 0 && class_exists('AITRFOEL_Logger')) {
            AITRFOEL_Logger::log(
                sprintf('Copied %d JetEngine field value(s) for post %d → %d', $copied, $original_id, $target_id),
                'info', $original_id, $target_id
            );
        }
    }

    /**
     * Whether a meta key should be skipped when copying custom fields to a
     * translation. Excludes WordPress internals, our own tracking meta, Elementor
     * data (the translation has its own translated copy), and SEO/featured-image
     * keys that are handled explicitly elsewhere in translate_post_meta_fields().
     *
     * @param string $key
     * @return bool
     */
    private function is_meta_copy_excluded($key) {
        if (!is_string($key) || $key === '') {
            return true;
        }
        // Internal / separately-handled keys, by prefix.
        $blocked_prefixes = [
            '_elementor', '_aitrfoel', '_edit_', '_wp_old', '_wp_page_template',
            '_yoast_wpseo', '_seopress', '_oembed_', '_wp_trash', '_menu_item', '_pingme',
            '_encloseme', '_thumbnail_id',
        ];
        $excluded = false;
        foreach ($blocked_prefixes as $bp) {
            if (strpos($key, $bp) === 0) {
                $excluded = true;
                break;
            }
        }
        // SEO (Rank Math) is translated separately.
        if (!$excluded && strpos($key, 'rank_math') === 0) {
            $excluded = true;
        }
        /**
         * Filter whether a meta key is excluded from being copied to a translation.
         * Return true to skip a key, or false to force-copy one that is excluded by
         * default.
         *
         * @param bool   $excluded
         * @param string $key
         */
        return (bool) apply_filters('aitrfoel_exclude_meta_from_copy', $excluded, $key);
    }

    /**
     * Copy every (non-excluded) custom-field value from the source post to the
     * translation, verbatim. Generic across JetEngine, ACF, Meta Box, Pods and plain
     * custom fields — ensures galleries, media, dates, numbers, relations and
     * repeaters are present on the translation. Text fields are translated afterwards
     * by the caller. Internal/handled keys (Elementor data, SEO, thumbnails, our
     * tracking meta) are skipped.
     *
     * @param int $original_id
     * @param int $target_id
     */
    private function copy_all_custom_meta($original_id, $target_id) {
        $all = get_post_meta($original_id);
        if (!is_array($all) || empty($all)) {
            return;
        }
        $copied = 0;
        foreach ($all as $key => $values) {
            if ($this->is_meta_copy_excluded($key) || !is_array($values)) {
                continue;
            }
            // Protected (underscore) keys are skipped, EXCEPT ACF field-key references
            // (value like "field_xxxx") which must travel with their value so ACF can
            // resolve the field on the translation.
            if ($key[0] === '_') {
                $first = isset($values[0]) ? $values[0] : '';
                if (!is_string($first) || strpos($first, 'field_') !== 0) {
                    continue;
                }
            }
            // Replace any existing values for this key on the translation, then add
            // each stored value (handles multi-value meta) in its original form.
            delete_post_meta($target_id, $key);
            foreach ($values as $raw) {
                $value = maybe_unserialize($raw);
                add_post_meta($target_id, $key, wp_slash($value));
                $copied++;
            }
        }
        if ($copied > 0 && class_exists('AITRFOEL_Logger')) {
            AITRFOEL_Logger::log(
                sprintf('Copied %d custom field value(s) for post %d → %d', $copied, $original_id, $target_id),
                'info', $original_id, $target_id
            );
        }
    }

    /**
     * Meta keys of ACF text-type fields (text / textarea / wysiwyg) attached to a
     * post, whose values should be translated. Returns [] when ACF is inactive.
     *
     * @param int $post_id
     * @return string[]
     */
    private function get_acf_text_meta_keys($post_id) {
        if (!function_exists('get_field_objects')) {
            return [];
        }
        $objects = get_field_objects($post_id, false, false);
        if (!is_array($objects) || empty($objects)) {
            return [];
        }
        $text_types = ['text', 'textarea', 'wysiwyg'];
        $keys = [];
        foreach ($objects as $name => $obj) {
            $type = is_array($obj) && isset($obj['type']) ? $obj['type'] : '';
            if (in_array($type, $text_types, true)) {
                $keys[] = is_string($name) ? $name : (isset($obj['name']) ? $obj['name'] : '');
            }
        }
        return array_values(array_filter($keys));
    }

    /**
     * Remap relational custom fields (ACF relationship / post_object / page_link, and
     * any plain meta whose value is a post ID) on the translation so they point at the
     * target-language translations of the referenced posts instead of the source ones.
     * No-op when no multilingual backend or no relational fields are found.
     *
     * @param int    $original_id
     * @param int    $target_id
     * @param string $target_lang
     */
    private function remap_relational_meta_fields($original_id, $target_id, $target_lang) {
        $mm = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null;
        if (!$mm || !method_exists($mm, 'get_post_translations')) {
            return;
        }

        // Collect relational ACF field meta keys (if ACF is present).
        $relational_keys = [];
        if (function_exists('get_field_objects')) {
            $objects = get_field_objects($original_id, false, false);
            if (is_array($objects)) {
                $relational_types = ['relationship', 'post_object', 'page_link'];
                foreach ($objects as $name => $obj) {
                    $type = is_array($obj) && isset($obj['type']) ? $obj['type'] : '';
                    if (in_array($type, $relational_types, true)) {
                        $relational_keys[] = is_string($name) ? $name : (isset($obj['name']) ? $obj['name'] : '');
                    }
                }
            }
        }

        /**
         * Filter the meta keys treated as post-ID references to remap to translations.
         * Useful for JetEngine relation fields, Meta Box post fields, or plain ID meta.
         *
         * @param string[] $relational_keys
         * @param int      $original_id
         * @param string   $target_lang
         */
        $relational_keys = apply_filters('aitrfoel_relational_meta_keys', $relational_keys, $original_id, $target_lang);
        $relational_keys = array_values(array_unique(array_filter((array) $relational_keys)));
        if (empty($relational_keys)) {
            return;
        }

        $remapped = 0;
        foreach ($relational_keys as $key) {
            if (!is_string($key) || $key === '') {
                continue;
            }
            $value = get_post_meta($target_id, $key, true); // copied source value
            $new   = $this->remap_relational_value($value, $target_lang, $mm, $changed);
            if (!empty($changed)) {
                update_post_meta($target_id, $key, $new);
                $remapped++;
            }
        }

        if ($remapped > 0 && class_exists('AITRFOEL_Logger')) {
            AITRFOEL_Logger::log(
                sprintf('Remapped %d relational field(s) to %s translations for post %d', $remapped, $target_lang, $target_id),
                'info', $original_id, $target_id
            );
        }
    }

    /**
     * Remap a relational field value (scalar ID or array of IDs) to the target-language
     * translations. Sets $changed by reference. Non-ID values (e.g. page_link URLs) are
     * returned unchanged.
     *
     * @param mixed                          $value
     * @param string                         $target_lang
     * @param AITRFOEL_Multilingual_Manager  $mm
     * @param bool|null                      $changed
     * @return mixed
     */
    private function remap_relational_value($value, $target_lang, $mm, &$changed = false) {
        $changed = false;
        if (is_array($value)) {
            $out = [];
            foreach ($value as $item) {
                $out[] = $this->remap_single_post_id($item, $target_lang, $mm, $changed);
            }
            return $out;
        }
        return $this->remap_single_post_id($value, $target_lang, $mm, $changed);
    }

    /**
     * If $id is a post ID with a translation in $target_lang, return the translation's
     * ID (as the same scalar type), flipping $changed to true; otherwise return $id.
     *
     * @param mixed                         $id
     * @param string                        $target_lang
     * @param AITRFOEL_Multilingual_Manager $mm
     * @param bool                          $changed
     * @return mixed
     */
    private function remap_single_post_id($id, $target_lang, $mm, &$changed) {
        if (!is_numeric($id) || (int) $id <= 0) {
            return $id;
        }
        $translations = $mm->get_post_translations((int) $id);
        if (is_array($translations) && isset($translations[$target_lang])) {
            $translated = (int) $translations[$target_lang];
            if ($translated > 0 && $translated !== (int) $id) {
                $changed = true;
                return is_string($id) ? (string) $translated : $translated;
            }
        }
        return $id;
    }

    /**
     * Heuristically detect custom-field meta keys whose values are human-readable
     * text worth translating, by inspecting the source post's actual meta. Used as a
     * fallback when field-framework definitions can't be read. Conservative: only
     * multi-word, non-token, non-URL, non-serialized, non-numeric string values
     * qualify, so slugs, CSS classes (e.g. "jet-engine-cpt-1"), IDs, dates, colors
     * and option keys are left alone.
     *
     * @param int $post_id
     * @return string[]
     */
    private function get_heuristic_text_meta_keys($post_id) {
        $all = get_post_meta($post_id);
        if (!is_array($all) || empty($all)) {
            return [];
        }
        $keys = [];
        foreach ($all as $key => $values) {
            if (!is_string($key) || $key === '' || $key[0] === '_') {
                continue; // private/internal keys are not user content
            }
            if ($this->is_meta_copy_excluded($key)) {
                continue;
            }
            $raw = isset($values[0]) ? $values[0] : '';
            if ($this->is_probably_translatable_text($raw)) {
                $keys[] = $key;
            }
        }
        return $keys;
    }

    /**
     * Whether a meta value looks like translatable human text (multi-word prose),
     * as opposed to a slug, id, class name, URL, email, number, date, color or
     * serialized structure.
     *
     * @param mixed $value
     * @return bool
     */
    private function is_probably_translatable_text($value) {
        if (!is_string($value)) {
            return false;
        }
        $t = trim($value);
        if (strlen($t) < 3) {
            return false;
        }
        if (is_numeric($t) || is_serialized($value)) {
            return false;
        }
        // JSON object/array.
        if (($t[0] === '{' || $t[0] === '[') && json_decode($t) !== null) {
            return false;
        }
        // URL or email.
        if (stripos($t, 'http://') === 0 || stripos($t, 'https://') === 0 || strpos($t, '://') !== false) {
            return false;
        }
        if (is_email($t)) {
            return false;
        }
        // Must contain a letter.
        if (!preg_match('/[A-Za-z\x80-\xff]/', $t)) {
            return false;
        }
        // Require multiple words (a space between word characters). Single tokens —
        // slugs, classes, enums, hex colors, ids — are skipped. Strip tags first so
        // wysiwyg/HTML still counts its inner words.
        $plain = trim(wp_strip_all_tags($t));
        if (strpos($plain, ' ') === false) {
            return false;
        }
        return true;
    }

    /**
     * Translatable custom-field text values for a post (JetEngine + ACF + heuristic),
     * after the translatable-keys filter. Public so the word-count estimator can include
     * custom-field text in quota calculations.
     *
     * @param int $post_id
     * @return string[]
     */
    public function get_translatable_meta_texts($post_id) {
        return array_values($this->get_translatable_meta_map($post_id));
    }

    /**
     * Translatable custom-field text for a post as [ meta_key => value ]. Used by the
     * review editor to present custom fields as editable segments, and by the word
     * estimator. Covers JetEngine + ACF + heuristic detection, after the filter.
     *
     * @param int $post_id
     * @return array<string,string>
     */
    public function get_translatable_meta_map($post_id) {
        $keys = array_values(array_unique(array_merge(
            $this->get_jetengine_text_meta_keys(get_post_type($post_id)),
            $this->get_acf_text_meta_keys($post_id),
            $this->get_heuristic_text_meta_keys($post_id)
        )));
        $keys = apply_filters('aitrfoel_translatable_meta_keys', $keys, $post_id, 0, '');
        if (!is_array($keys)) {
            return [];
        }
        $map = [];
        foreach ($keys as $key) {
            if (!is_string($key) || $key === '') {
                continue;
            }
            $val = get_post_meta($post_id, $key, true);
            if (is_string($val) && trim($val) !== '') {
                $map[$key] = $val;
            }
        }
        return $map;
    }

    private function translate_post_meta_fields($original_id, $target_id, $target_lang, $source_lang = 'auto') {
        // Copy all custom-field values (galleries, media, dates, numbers, relations,
        // repeaters and text) from the source to the translation first, so nothing is
        // missing on the translation. This covers JetEngine, ACF, Meta Box, Pods and
        // plain custom fields generically. Text-type fields are overwritten with their
        // translations further down. Internal/handled keys are excluded.
        $this->copy_all_custom_meta($original_id, $target_id);

        // Keep featured image in sync for translated posts/pages/products.
        $thumbnail_id = get_post_thumbnail_id($original_id);
        if ($thumbnail_id) {
            set_post_thumbnail($target_id, (int) $thumbnail_id);
        } else {
            delete_post_thumbnail($target_id);
        }

        $seo_translate_keys = [];

        $seo_enabled = get_option('aitrfoel_seo_translation_enabled', true);
        $seo_og_enabled = get_option('aitrfoel_seo_translate_og', true);
        $seo_twitter_enabled = get_option('aitrfoel_seo_translate_twitter', true);

        // Yoast SEO
        if ($seo_enabled && (defined('WPSEO_VERSION') || class_exists('WPSEO_Meta'))) {
            $seo_translate_keys[] = '_yoast_wpseo_title';
            $seo_translate_keys[] = '_yoast_wpseo_metadesc';

            if ($seo_og_enabled) {
                $seo_translate_keys[] = '_yoast_wpseo_opengraph-title';
                $seo_translate_keys[] = '_yoast_wpseo_opengraph-description';
            }
            if ($seo_twitter_enabled) {
                $seo_translate_keys[] = '_yoast_wpseo_twitter-title';
                $seo_translate_keys[] = '_yoast_wpseo_twitter-description';
            }

            $focus_kw = get_post_meta($original_id, '_yoast_wpseo_focuskw', true);
            if ($focus_kw !== '') {
                update_post_meta($target_id, '_yoast_wpseo_focuskw', $focus_kw);
            }
        }

        // Rank Math
        if ($seo_enabled && (class_exists('RankMath') || defined('RANK_MATH_VERSION'))) {
            $seo_translate_keys[] = 'rank_math_title';
            $seo_translate_keys[] = 'rank_math_description';

            if ($seo_og_enabled) {
                $seo_translate_keys[] = 'rank_math_og_title';
                $seo_translate_keys[] = 'rank_math_og_description';
            }
            if ($seo_twitter_enabled) {
                $seo_translate_keys[] = 'rank_math_twitter_title';
                $seo_translate_keys[] = 'rank_math_twitter_description';
            }

            $rm_kw = get_post_meta($original_id, 'rank_math_focus_keyword', true);
            if ($rm_kw !== '') {
                update_post_meta($target_id, 'rank_math_focus_keyword', $rm_kw);
            }
        }

        // SEOPress
        if ($seo_enabled && (defined('SEOPRESS_VERSION') || function_exists('seopress_init'))) {
            $seo_translate_keys[] = '_seopress_titles_title';
            $seo_translate_keys[] = '_seopress_titles_desc';

            if ($seo_og_enabled) {
                $seo_translate_keys[] = '_seopress_social_og_title';
                $seo_translate_keys[] = '_seopress_social_og_desc';
            }
            if ($seo_twitter_enabled) {
                $seo_translate_keys[] = '_seopress_social_twitter_title';
                $seo_translate_keys[] = '_seopress_social_twitter_desc';
            }
        }

        // Collect non-empty values for batch translation
        $to_translate = [];
        foreach ($seo_translate_keys as $key) {
            $val = get_post_meta($original_id, $key, true);
            if (!empty($val)) {
                $to_translate[$key] = $val;
            }
        }

        // Translate post excerpt (also serves as WooCommerce short description).
        // Primary source is the original post's excerpt. Fallback: if the source
        // excerpt is empty but the TARGET carries an untranslated excerpt (common
        // for older translations where the source-language text was copied onto
        // the translation and never translated), translate that text in place.
        $original_post = get_post($original_id);
        $original_excerpt = $original_post ? trim((string) $original_post->post_excerpt) : '';

        if ($original_excerpt !== '') {
            $to_translate['__excerpt__'] = $original_excerpt;
        } else {
            $target_post = get_post($target_id);
            $target_excerpt = $target_post ? trim((string) $target_post->post_excerpt) : '';
            if ($target_excerpt !== '') {
                $to_translate['__excerpt__'] = $target_excerpt;
            }
        }

        // Custom-field text: many CPTs store their real content in text/textarea/
        // wysiwyg meta fields (rendered via JetEngine Listings, ACF blocks, dynamic
        // tags), leaving post_content empty. Translate those text fields so the
        // content — not just the title — gets translated. Non-text fields were copied
        // verbatim above. Covers JetEngine and ACF field definitions.
        $custom_text_keys = array_merge(
            $this->get_jetengine_text_meta_keys(get_post_type($original_id)),
            $this->get_acf_text_meta_keys($original_id),
            // Heuristic fallback: detect human-readable text in any custom field even
            // when the framework's field definitions can't be read. Covers JetEngine
            // setups where the meta-box config isn't in the expected option, plus Pods,
            // Meta Box, and plain custom fields.
            $this->get_heuristic_text_meta_keys($original_id)
        );
        $custom_text_keys = array_values(array_unique($custom_text_keys));
        /**
         * Filter which custom-field meta keys are translated for a post. Add keys the
         * auto-detection missed, or remove keys that should stay in the source language.
         *
         * @param string[] $custom_text_keys
         * @param int      $original_id
         * @param int      $target_id
         * @param string   $target_lang
         */
        $custom_text_keys = apply_filters('aitrfoel_translatable_meta_keys', $custom_text_keys, $original_id, $target_id, $target_lang);
        if (!is_array($custom_text_keys)) {
            $custom_text_keys = [];
        }
        foreach ($custom_text_keys as $custom_key) {
            if (!is_string($custom_key) || $custom_key === '' || isset($to_translate[$custom_key])) {
                continue;
            }
            $custom_val = get_post_meta($original_id, $custom_key, true);
            if (is_string($custom_val) && trim($custom_val) !== '') {
                $to_translate[$custom_key] = $custom_val;
            }
        }

        if (!empty($to_translate)) {
            $api = AITRFOEL_OpenAI_API::get_instance();
            $texts = array_values($to_translate);
            $keys  = array_keys($to_translate);

            $translated = $api->translate_batch($texts, $target_lang, $source_lang);

            if (!is_wp_error($translated) && is_array($translated)) {
                // translate_batch() returns an associative map [ original_text => translation ],
                // NOT a positional array — so look up each field's translation by its source
                // value, not by index. (A positional lookup silently writes nothing.)
                $written = [];
                foreach ($keys as $idx => $key) {
                    $original = isset($texts[$idx]) ? $texts[$idx] : null;
                    if ($original === null || !isset($translated[$original])) {
                        continue;
                    }
                    $translated_text = $translated[$original];
                    if (is_wp_error($translated_text) || $translated_text === '' || $translated_text === null) {
                        continue;
                    }
                    if ($key === '__excerpt__') {
                        wp_update_post(['ID' => $target_id, 'post_excerpt' => $translated_text]);
                    } else {
                        update_post_meta($target_id, $key, $translated_text);
                    }
                    $written[] = $key;
                }

                AITRFOEL_Logger::log(
                    sprintf('Translated %d SEO/meta field(s) for post %d → %d (%s): %s', count($written), $original_id, $target_id, $target_lang, implode(', ', $written)),
                    'info', $original_id, $target_id
                );
            }
        }

        // ACF (and similar) relational fields store post IDs pointing at the source-
        // language items. Remap them to the target-language translation so e.g. a
        // "related projects" field links to translated posts instead of the originals.
        $this->remap_relational_meta_fields($original_id, $target_id, $target_lang);

        // WooCommerce: copy non-translatable product meta
        if (get_post_type($original_id) === 'product' && get_option('aitrfoel_woocommerce_support', true)) {
            $wc_copy_keys = [
                '_regular_price', '_sale_price', '_price', '_sku',
                '_stock_status', '_manage_stock', '_stock',
                '_weight', '_length', '_width', '_height',
                '_virtual', '_downloadable', '_product_image_gallery',
                '_thumbnail_id', 'total_sales',
            ];
            foreach ($wc_copy_keys as $key) {
                $val = get_post_meta($original_id, $key, true);
                if ($val !== '') {
                    update_post_meta($target_id, $key, $val);
                }
            }
        }
    }

    /**
     * Public entry point for the SEO meta patch tool.
     * Re-translates SEO fields (Yoast / Rank Math title, description, OG, Twitter)
     * and the post excerpt for an existing translation pair.
     *
     * @param int    $original_id  Source (default-language) post ID
     * @param int    $target_id    Translated post ID
     * @param string $target_lang  Target language code (e.g. 'en', 'de')
     * @param string $source_lang  Source language code or 'auto'
     */
    public function patch_seo_meta_fields($original_id, $target_id, $target_lang, $source_lang = 'auto') {
        $this->translate_post_meta_fields($original_id, $target_id, $target_lang, $source_lang);
    }

    /**
     * Assign translated WooCommerce product categories and tags to a translated product.
     * Uses Polylang/WPML term translation lookup to find equivalent terms.
     * For the built-in language system, assigns original terms and translates their names.
     *
     * @param int    $original_id  Source product ID
     * @param int    $translated_id Translated product ID
     * @param string $target_lang  Target language code (2-letter)
     */
    private function translate_product_taxonomies($original_id, $translated_id, $target_lang) {
        if (get_post_type($original_id) !== 'product') {
            return;
        }

        $woo_enabled = get_option('aitrfoel_woocommerce_support', true);
        if (!$woo_enabled) {
            return;
        }

        $taxonomies = [];
        if (get_option('aitrfoel_woo_translate_categories', true)) {
            $taxonomies[] = 'product_cat';
        }
        if (get_option('aitrfoel_woo_translate_tags', true)) {
            $taxonomies[] = 'product_tag';
        }

        if (empty($taxonomies)) {
            return;
        }

        $this->assign_and_translate_terms($original_id, $translated_id, $target_lang, $taxonomies);
    }

    /**
     * Assign and translate taxonomy terms (categories/tags) for any post type.
     * Handles Polylang/WPML term lookups, and for the built-in language system
     * assigns the original terms and stores translated names as term meta.
     *
     * @param int    $original_id   Source post ID
     * @param int    $translated_id Translated post ID
     * @param string $target_lang   Target language code (2-letter)
     * @param array  $taxonomies    List of taxonomy slugs to process
     */
    private function assign_and_translate_terms($original_id, $translated_id, $target_lang, $taxonomies) {
        $terms_to_translate = []; // Collect term names for batch translation

        foreach ($taxonomies as $taxonomy) {
            $terms = wp_get_post_terms($original_id, $taxonomy, ['fields' => 'all']);
            if (empty($terms) || is_wp_error($terms)) {
                continue;
            }

            $translated_term_ids = [];
            foreach ($terms as $term) {
                $translated_term_id = null;

                // Polylang
                if (function_exists('pll_get_term')) {
                    $translated_term_id = pll_get_term($term->term_id, $target_lang);
                }

                // WPML fallback
                if (!$translated_term_id && function_exists('apply_filters')) {
                    $trid = apply_filters('wpml_element_trid', null, $term->term_taxonomy_id, 'tax_' . $taxonomy);
                    if ($trid) {
                        $translations = apply_filters('wpml_get_element_translations', null, $trid, 'tax_' . $taxonomy);
                        if (isset($translations[$target_lang])) {
                            $translated_term_id = $translations[$target_lang]->term_id;
                        }
                    }
                }

                // Built-in language system fallback: assign original term
                if (!$translated_term_id) {
                    $translated_term_id = $term->term_id;

                    // Queue term name for translation if not already translated
                    $existing = get_term_meta($term->term_id, '_aitrfoel_name_' . $target_lang, true);
                    if (empty($existing) && get_option('aitrfoel_translate_taxonomy_names', true)) {
                        $terms_to_translate[$term->term_id] = $term->name;
                    }
                }

                if ($translated_term_id) {
                    $translated_term_ids[] = (int) $translated_term_id;
                }
            }

            if (!empty($translated_term_ids)) {
                wp_set_post_terms($translated_id, $translated_term_ids, $taxonomy);
                AITRFOEL_Logger::log(
                    sprintf('Assigned %d %s terms to post %d', count($translated_term_ids), $taxonomy, $translated_id),
                    'info', $original_id, $translated_id
                );
            }
        }

        // Batch-translate collected term names
        if (!empty($terms_to_translate)) {
            $this->translate_term_names_batch($terms_to_translate, $target_lang);
        }
    }

    /**
     * Translate term names via API and store as term meta.
     *
     * @param array  $terms_map   [term_id => term_name] pairs
     * @param string $target_lang Target language code
     */
    private function translate_term_names_batch($terms_map, $target_lang) {
        $api = AITRFOEL_OpenAI_API::get_instance();
        $names = array_values($terms_map);
        $term_ids = array_keys($terms_map);

        $translated = $api->translate_batch($names, $target_lang);
        if (is_wp_error($translated) || !is_array($translated)) {
            AITRFOEL_Logger::log(
                'Failed to translate taxonomy term names: ' . (is_wp_error($translated) ? $translated->get_error_message() : 'unknown error'),
                'warning'
            );
            return;
        }

        // translate_batch returns [original => translated] mapping
        foreach ($names as $i => $original_name) {
            if (!isset($term_ids[$i])) continue;
            $term_id = $term_ids[$i];
            $translated_name = isset($translated[$original_name]) ? $translated[$original_name] : null;

            if ($translated_name && !is_wp_error($translated_name) && $translated_name !== $original_name) {
                update_term_meta($term_id, '_aitrfoel_name_' . $target_lang, sanitize_text_field($translated_name));
                AITRFOEL_Logger::log(
                    sprintf('Translated term "%s" → "%s" (%s, term ID %d)', $original_name, $translated_name, $target_lang, $term_id),
                    'info'
                );
            }
        }
    }

    /**
     * Translate and assign regular post categories and tags.
     * Called for non-WooCommerce posts during translation.
     *
     * @param int    $original_id   Source post ID
     * @param int    $translated_id Translated post ID
     * @param string $target_lang   Target language code
     */
    private function translate_post_taxonomies($original_id, $translated_id, $target_lang) {
        $post_type = get_post_type($original_id);

        // Skip WooCommerce products (handled by translate_product_taxonomies)
        if ($post_type === 'product') {
            return;
        }

        $taxonomies = get_object_taxonomies($post_type, 'names');
        // Filter to only public, non-internal taxonomies
        $translatable = [];
        foreach ($taxonomies as $tax) {
            $tax_obj = get_taxonomy($tax);
            if ($tax_obj && $tax_obj->public && $tax_obj->show_ui && $tax !== 'aitrfoel_language') {
                $translatable[] = $tax;
            }
        }

        if (empty($translatable)) {
            return;
        }

        $this->assign_and_translate_terms($original_id, $translated_id, $target_lang, $translatable);
    }

    /**
     * Translate WooCommerce product attributes (_product_attributes meta).
     * Handles both custom (local) attributes and global taxonomy (pa_*) attributes.
     *
     * @param int    $original_id   Source product ID
     * @param int    $translated_id Translated product ID
     * @param string $target_lang   Target language code (2-letter)
     * @param string $source_lang   Source language code
     */
    private function translate_product_attributes($original_id, $translated_id, $target_lang, $source_lang = 'auto') {
        if (get_post_type($original_id) !== 'product') {
            return;
        }
        if (!get_option('aitrfoel_woocommerce_support', true) || !get_option('aitrfoel_woo_translate_attributes', true)) {
            return;
        }

        $attributes = get_post_meta($original_id, '_product_attributes', true);
        if (empty($attributes) || !is_array($attributes)) {
            return;
        }

        $api = AITRFOEL_OpenAI_API::get_instance();
        $translated_attributes = [];
        $texts_to_translate = [];
        $text_keys = []; // Track which attribute+field each text belongs to

        foreach ($attributes as $attr_key => $attr_data) {
            $translated_attributes[$attr_key] = $attr_data;

            if (!empty($attr_data['is_taxonomy']) && taxonomy_exists($attr_key)) {
                // Global taxonomy attribute (pa_color, pa_size, etc.)
                // Assign translated terms via Polylang/WPML
                $terms = wp_get_post_terms($original_id, $attr_key, ['fields' => 'all']);
                if (!empty($terms) && !is_wp_error($terms)) {
                    $translated_term_ids = [];
                    foreach ($terms as $term) {
                        $translated_term_id = null;

                        if (function_exists('pll_get_term')) {
                            $translated_term_id = pll_get_term($term->term_id, $target_lang);
                        }

                        if (!$translated_term_id) {
                            $trid = apply_filters('wpml_element_trid', null, $term->term_taxonomy_id, 'tax_' . $attr_key);
                            if ($trid) {
                                $translations = apply_filters('wpml_get_element_translations', null, $trid, 'tax_' . $attr_key);
                                if (isset($translations[$target_lang])) {
                                    $translated_term_id = $translations[$target_lang]->term_id;
                                }
                            }
                        }

                        if ($translated_term_id) {
                            $translated_term_ids[] = (int) $translated_term_id;
                        } else {
                            // Fallback: keep original term
                            $translated_term_ids[] = $term->term_id;
                        }
                    }
                    wp_set_post_terms($translated_id, $translated_term_ids, $attr_key);

                    // Update the value field with translated term slugs
                    $translated_terms_obj = array_map('get_term', $translated_term_ids);
                    $slugs = wp_list_pluck(array_filter($translated_terms_obj), 'slug');
                    $translated_attributes[$attr_key]['value'] = implode(' | ', $slugs);
                }
            } else {
                // Custom (local) attribute — translate the name and pipe-delimited values
                if (!empty($attr_data['name'])) {
                    $texts_to_translate[] = $attr_data['name'];
                    $text_keys[] = ['attr' => $attr_key, 'field' => 'name'];
                }
                if (!empty($attr_data['value'])) {
                    $texts_to_translate[] = $attr_data['value'];
                    $text_keys[] = ['attr' => $attr_key, 'field' => 'value'];
                }
            }
        }

        // Batch-translate custom attribute names and values
        if (!empty($texts_to_translate)) {
            $translated_texts = $api->translate_batch($texts_to_translate, $target_lang, $source_lang);
            if (!is_wp_error($translated_texts) && is_array($translated_texts)) {
                foreach ($translated_texts as $i => $translated_text) {
                    if (!isset($text_keys[$i]) || is_wp_error($translated_text)) {
                        continue;
                    }
                    $attr = $text_keys[$i]['attr'];
                    $field = $text_keys[$i]['field'];
                    $translated_attributes[$attr][$field] = $translated_text;
                }
            }
        }

        update_post_meta($translated_id, '_product_attributes', $translated_attributes);

        AITRFOEL_Logger::log(
            sprintf('Translated %d product attributes for product %d → %d', count($attributes), $original_id, $translated_id),
            'info', $original_id, $translated_id
        );
    }

    /**
     * Translate WooCommerce product variations for variable products.
     * Creates translated variation posts with proper parent-child relationships.
     *
     * @param int    $original_id   Source product ID
     * @param int    $translated_id Translated product ID
     * @param string $target_lang   Target language code (2-letter)
     * @param string $source_lang   Source language code
     */
    private function translate_product_variations($original_id, $translated_id, $target_lang, $source_lang = 'auto') {
        if (get_post_type($original_id) !== 'product') {
            return;
        }
        if (!get_option('aitrfoel_woocommerce_support', true) || !get_option('aitrfoel_woo_translate_variations', true)) {
            return;
        }

        // Only process variable products
        $product_type = wp_get_post_terms($original_id, 'product_type', ['fields' => 'slugs']);
        if (empty($product_type) || !in_array('variable', $product_type, true)) {
            return;
        }

        // Set the translated product as variable type too
        wp_set_object_terms($translated_id, 'variable', 'product_type');

        // Get source variations
        $variations = get_posts([
            'post_type'   => 'product_variation',
            'post_parent' => $original_id,
            'numberposts' => -1,
            'post_status' => 'any',
            'orderby'     => 'menu_order',
            'order'       => 'ASC',
        ]);

        if (empty($variations)) {
            return;
        }

        // Delete existing variations on the translated product to avoid duplicates on re-translation
        $existing_variations = get_posts([
            'post_type'   => 'product_variation',
            'post_parent' => $translated_id,
            'numberposts' => -1,
            'post_status' => 'any',
            'fields'      => 'ids',
        ]);
        foreach ($existing_variations as $ev_id) {
            wp_delete_post($ev_id, true);
        }

        // Collect translatable variation descriptions for batch translation
        $descriptions_to_translate = [];
        $desc_variation_indices = [];
        foreach ($variations as $idx => $variation) {
            $desc = $variation->post_excerpt;
            if (!empty(trim($desc))) {
                $descriptions_to_translate[] = $desc;
                $desc_variation_indices[] = $idx;
            }
        }

        // Batch-translate descriptions
        $translated_descriptions = [];
        if (!empty($descriptions_to_translate)) {
            $api = AITRFOEL_OpenAI_API::get_instance();
            $result = $api->translate_batch($descriptions_to_translate, $target_lang, $source_lang);
            if (!is_wp_error($result) && is_array($result)) {
                foreach ($result as $i => $td) {
                    if (isset($desc_variation_indices[$i]) && !is_wp_error($td)) {
                        $translated_descriptions[$desc_variation_indices[$i]] = $td;
                    }
                }
            }
        }

        // Meta keys to copy directly (non-translatable variation data)
        $variation_copy_keys = [
            '_regular_price', '_sale_price', '_price', '_sku',
            '_stock_status', '_manage_stock', '_stock',
            '_weight', '_length', '_width', '_height',
            '_virtual', '_downloadable', '_download_limit', '_download_expiry',
            '_thumbnail_id', '_variation_description',
        ];

        foreach ($variations as $idx => $variation) {
            // Create the translated variation
            $new_variation_id = wp_insert_post([
                'post_type'    => 'product_variation',
                'post_parent'  => $translated_id,
                'post_status'  => $variation->post_status,
                'post_title'   => $variation->post_title,
                'post_excerpt' => isset($translated_descriptions[$idx]) ? $translated_descriptions[$idx] : $variation->post_excerpt,
                'menu_order'   => $variation->menu_order,
            ]);

            if (is_wp_error($new_variation_id) || !$new_variation_id) {
                continue;
            }

            // Copy variation meta
            foreach ($variation_copy_keys as $key) {
                $val = get_post_meta($variation->ID, $key, true);
                if ($val !== '') {
                    update_post_meta($new_variation_id, $key, $val);
                }
            }

            // Copy attribute assignment meta (attribute_pa_color, attribute_pa_size, etc.)
            $all_meta = get_post_meta($variation->ID);
            foreach ($all_meta as $meta_key => $meta_values) {
                if (strpos($meta_key, 'attribute_') === 0) {
                    $attr_value = $meta_values[0];
                    $taxonomy = str_replace('attribute_', '', $meta_key);

                    // For taxonomy attributes, try to find translated term slug
                    if (taxonomy_exists($taxonomy) && !empty($attr_value)) {
                        $term = get_term_by('slug', $attr_value, $taxonomy);
                        if ($term) {
                            $translated_term_id = null;
                            if (function_exists('pll_get_term')) {
                                $translated_term_id = pll_get_term($term->term_id, $target_lang);
                            }
                            if (!$translated_term_id) {
                                $trid = apply_filters('wpml_element_trid', null, $term->term_taxonomy_id, 'tax_' . $taxonomy);
                                if ($trid) {
                                    $translations = apply_filters('wpml_get_element_translations', null, $trid, 'tax_' . $taxonomy);
                                    if (isset($translations[$target_lang])) {
                                        $translated_term_id = $translations[$target_lang]->term_id;
                                    }
                                }
                            }
                            if ($translated_term_id) {
                                $translated_term = get_term($translated_term_id, $taxonomy);
                                if ($translated_term && !is_wp_error($translated_term)) {
                                    $attr_value = $translated_term->slug;
                                }
                            }
                        }
                    }
                    update_post_meta($new_variation_id, $meta_key, $attr_value);
                }
            }
        }

        // Copy variable product transient data
        $default_attributes = get_post_meta($original_id, '_default_attributes', true);
        if (!empty($default_attributes) && is_array($default_attributes)) {
            $translated_defaults = [];
            foreach ($default_attributes as $taxonomy => $slug) {
                if (taxonomy_exists($taxonomy) && !empty($slug)) {
                    $term = get_term_by('slug', $slug, $taxonomy);
                    if ($term) {
                        $translated_term_id = null;
                        if (function_exists('pll_get_term')) {
                            $translated_term_id = pll_get_term($term->term_id, $target_lang);
                        }
                        if ($translated_term_id) {
                            $translated_term = get_term($translated_term_id, $taxonomy);
                            if ($translated_term && !is_wp_error($translated_term)) {
                                $translated_defaults[$taxonomy] = $translated_term->slug;
                                continue;
                            }
                        }
                    }
                }
                $translated_defaults[$taxonomy] = $slug;
            }
            update_post_meta($translated_id, '_default_attributes', $translated_defaults);
        }

        // Clean transients so WooCommerce rebuilds variation data
        delete_transient('wc_product_children_' . $translated_id);
        delete_transient('wc_var_prices_' . $translated_id);

        AITRFOEL_Logger::log(
            sprintf('Created %d translated variations for variable product %d → %d', count($variations), $original_id, $translated_id),
            'info', $original_id, $translated_id
        );
    }

    /**
     * Translate product gallery and featured image alt text, captions, and descriptions.
     *
     * @param int    $original_id   Source product ID
     * @param int    $translated_id Translated product ID
     * @param string $target_lang   Target language code (2-letter)
     * @param string $source_lang   Source language code
     */
    private function translate_product_gallery_meta($original_id, $translated_id, $target_lang, $source_lang = 'auto') {
        if (get_post_type($original_id) !== 'product') {
            return;
        }
        if (!get_option('aitrfoel_woocommerce_support', true) || !get_option('aitrfoel_woo_translate_gallery_alt', true)) {
            return;
        }

        // Collect all image IDs: featured image + gallery images
        $image_ids = [];
        $thumbnail_id = get_post_meta($original_id, '_thumbnail_id', true);
        if ($thumbnail_id) {
            $image_ids[] = (int) $thumbnail_id;
        }
        $gallery_ids = get_post_meta($original_id, '_product_image_gallery', true);
        if (!empty($gallery_ids)) {
            $gallery_arr = array_map('intval', array_filter(explode(',', $gallery_ids)));
            $image_ids = array_merge($image_ids, $gallery_arr);
        }
        $image_ids = array_unique($image_ids);

        if (empty($image_ids)) {
            return;
        }

        // Collect translatable text from images
        $texts_to_translate = [];
        $text_keys = []; // [image_id, field]
        foreach ($image_ids as $img_id) {
            $alt = get_post_meta($img_id, '_wp_attachment_image_alt', true);
            if (!empty(trim($alt))) {
                $texts_to_translate[] = $alt;
                $text_keys[] = ['id' => $img_id, 'field' => 'alt'];
            }

            $attachment = get_post($img_id);
            if ($attachment) {
                if (!empty(trim($attachment->post_excerpt))) {
                    $texts_to_translate[] = $attachment->post_excerpt;
                    $text_keys[] = ['id' => $img_id, 'field' => 'caption'];
                }
                if (!empty(trim($attachment->post_content))) {
                    $texts_to_translate[] = $attachment->post_content;
                    $text_keys[] = ['id' => $img_id, 'field' => 'description'];
                }
            }
        }

        if (empty($texts_to_translate)) {
            return;
        }

        // Batch-translate all image text
        $api = AITRFOEL_OpenAI_API::get_instance();
        $translated_texts = $api->translate_batch($texts_to_translate, $target_lang, $source_lang);
        if (is_wp_error($translated_texts) || !is_array($translated_texts)) {
            return;
        }

        // Apply translations — we store translated alt/caption/description as post meta on the translated product
        // This avoids modifying the shared attachment posts (which are shared across languages)
        $translated_image_meta = [];
        foreach ($translated_texts as $i => $translated_text) {
            if (!isset($text_keys[$i]) || is_wp_error($translated_text)) {
                continue;
            }
            $img_id = $text_keys[$i]['id'];
            $field = $text_keys[$i]['field'];
            if (!isset($translated_image_meta[$img_id])) {
                $translated_image_meta[$img_id] = [];
            }
            $translated_image_meta[$img_id][$field] = $translated_text;
        }

        // Store as structured meta on the translated product for theme/frontend retrieval
        update_post_meta($translated_id, '_aitrfoel_translated_image_meta', $translated_image_meta);

        // Also update the alt text directly on attachment posts if they don't have language-specific versions
        // This is safe because alt text meta can be overwritten and is the most impactful for SEO
        foreach ($translated_image_meta as $img_id => $fields) {
            if (isset($fields['alt'])) {
                // Store translated alt with a language-specific key to avoid overwriting the original
                update_post_meta($img_id, '_wp_attachment_image_alt_' . $target_lang, sanitize_text_field($fields['alt']));
            }
        }

        AITRFOEL_Logger::log(
            sprintf('Translated image meta for %d images on product %d → %d', count($translated_image_meta), $original_id, $translated_id),
            'info', $original_id, $translated_id
        );
    }

    /**
     * Post-creation processing for translated pages
     * Handles cache warming, HTML regeneration, and LiteSpeed optimizations
     *
     * @param int $page_id Translated page ID
     * @return void
     */
    public function post_creation_processing($page_id) {
        AITRFOEL_Logger::log("Starting post-creation processing for page $page_id", 'info', $page_id);
        
        // Step 1: Force Elementor to regenerate HTML content
        if (class_exists('\Elementor\Plugin')) {
            try {
                // Get the document for this page
                $document = \Elementor\Plugin::$instance->documents->get($page_id);
                
                if ($document && !is_wp_error($document)) {
                    AITRFOEL_Logger::log("Regenerating Elementor HTML for page $page_id", 'info', $page_id);

                    // Force save to regenerate HTML - this updates post_content.
                    // Buffer it: Elementor can print regenerated CSS (atomic container
                    // variables, etc.) during save, which would otherwise leak into an
                    // AJAX response and corrupt the JSON ("request failed").
                    ob_start();
                    $document->save([]);
                    ob_end_clean();
                    
                    // Alternative method for older Elementor versions
                    if (method_exists($document, 'get_main_post')) {
                        $post = $document->get_main_post();
                        if ($post) {
                            // Get the latest Elementor data and regenerate HTML
                            $elementor_data = get_post_meta($page_id, '_elementor_data', true);
                            if ($elementor_data) {
                                // Update post content with fresh HTML
                                $updated_content = $document->get_content();
                                wp_update_post([
                                    'ID' => $page_id,
                                    'post_content' => $updated_content
                                ]);
                                AITRFOEL_Logger::log("Updated post_content with regenerated HTML", 'info', $page_id);
                            }
                        }
                    }
                } else {
                    AITRFOEL_Logger::log("Could not get Elementor document for page $page_id", 'warning', $page_id);
                }
            } catch (Exception $e) {
                AITRFOEL_Logger::log("Elementor regeneration failed: " . $e->getMessage(), 'error', $page_id);
            }
        }
        
        // Step 2: Clear all relevant caches
        AITRFOEL_Logger::log("Clearing caches for page $page_id", 'info', $page_id);
        
        // Clear Elementor cache
        if (class_exists('\Elementor\Plugin')) {
            \Elementor\Plugin::$instance->files_manager->clear_cache();
        }
        
        // Clear LiteSpeed cache if available
        if (defined('LSCWP_V') && class_exists('LiteSpeed\Purge')) {
            try {
                // Purge specific page
                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party LiteSpeed Cache hook
                do_action('litespeed_purge_single_post', $page_id);
                AITRFOEL_Logger::log("LiteSpeed cache purged for page $page_id", 'info', $page_id);
            } catch (Exception $e) {
                AITRFOEL_Logger::log("LiteSpeed cache purge failed: " . $e->getMessage(), 'warning', $page_id);
            }
        }
        
        // Step 3: REMOVED sleep(2) for performance - database consistency is instant
        // Note: Removed 2-second delay that was slowing down translations
        
        // Step 4: REMOVED cache warming for performance (saves 3-5 seconds per translation)
        // Reason: wp_remote_get() triggers new PHP process → Freemius initialization → 3-5s delay
        // Impact: LiteSpeed will cache page on first real visitor, which is acceptable
        // Note: Cache warming was adding 5-10% overhead to translation time
        AITRFOEL_Logger::log("Skipping cache warm-up for performance (cache will build on first visitor)", 'info', $page_id);
        
        // Step 5: Additional LiteSpeed-specific optimizations
        if (defined('LSCWP_V')) {
            AITRFOEL_Logger::log("Triggering LiteSpeed post-processing hooks", 'info', $page_id);
            
            // Trigger LiteSpeed's optimization pipeline
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party LiteSpeed Cache hook
            do_action('litespeed_cache_finalize');
            
            // Force image optimization check
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party LiteSpeed Cache hook
            do_action('litespeed_media_finalize', $page_id);
        }
        
        AITRFOEL_Logger::log("Post-creation processing completed for page $page_id", 'success', $page_id);
    }

    /**
     * Detect source language from strings
     * 
     * @param array $strings Array of strings to analyze
     * @return string|null Detected language code or null on failure
     */
    private function detect_source_language($strings, $page_id = null) {
        // Get the API instance (OpenAI only now)
        $api = AITRFOEL_OpenAI_API::get_instance();
        
        $sample_parts = [];
        $total_len = 0;
        foreach ($strings as $item) {
            if (!isset($item['original'])) continue;
            $t = wp_strip_all_tags($item['original']);
            if ($t === '') continue;
            $sample_parts[] = $t;
            $total_len += strlen($t);
            if ($total_len > 800) break; // enough sample
        }
        if (empty($sample_parts)) return null;
        $sample_text = implode("\n", $sample_parts);
        $detected = $api->detect_language($sample_text);
        if (is_wp_error($detected)) {
            AITRFOEL_Logger::log('Language auto-detect failed: ' . $detected->get_error_message(), 'warning', $page_id);
            return null;
        }
        AITRFOEL_Logger::log('Detected source language: ' . $detected, 'info', $page_id);
        return $detected;
    }

    /**
     * Get last translation statistics
     * 
     * @return array Translation statistics
     */
    public function get_last_translation_stats() {
        return $this->last_translation_stats ?? [
            'elements_translated' => 0,
            'word_count' => 0,
            'input_tokens' => 0,
            'output_tokens' => 0,
            'total_tokens' => 0,
            'estimated_cost' => 0,
            'cost_formatted' => '$0.0000',
            'is_estimated' => true,
            'cached_strings' => 0,
            'uncached_strings' => 0,
            'usage_records' => []
        ];
    }
    
    /**
     * Count words in an Elementor element recursively
     * 
     * @param array $element Element data
     * @return int Word count
     */
    private function count_element_words($element) {
        $words = 0;
        if (is_array($element)) {
            if (isset($element['settings'])) {
                foreach ($element['settings'] as $value) {
                    if (is_string($value)) {
                        $words += self::count_words(wp_strip_all_tags($value));
                    }
                }
            }
            if (isset($element['elements']) && is_array($element['elements'])) {
                foreach ($element['elements'] as $child) {
                    $words += $this->count_element_words($child);
                }
            }
        }
        return $words;
    }
    
    /**
     * Validate translation quality
     * 
     * @param array $translations Array of translations
     * @param string $target_lang Target language
     * @return array Array of quality issues
     */
    private function validate_translation_quality($translations, $target_lang) {
        $issues = [];
        $suspicious_count = 0;
        $total_count = count($translations);
        
        // Character set validation for different languages
        $charset_patterns = [
            'ru' => '/[а-яё]/ui',          // Cyrillic for Russian
            'cs' => '/[ěščřžýáíéúů]/ui',   // Czech specific characters
            'pt' => '/[ãõçáéíóúâêôà]/ui', // Portuguese specific characters
            'zh' => '/[\x{4e00}-\x{9fff}]/u', // Chinese characters
            'ar' => '/[\x{0600}-\x{06ff}]/u', // Arabic characters
            'ja' => '/[\x{3040}-\x{309f}\x{30a0}-\x{30ff}\x{4e00}-\x{9faf}]/u', // Japanese
            'ko' => '/[\x{ac00}-\x{d7af}]/u', // Korean Hangul
            'th' => '/[\x{0e00}-\x{0e7f}]/u', // Thai
            'hi' => '/[\x{0900}-\x{097f}]/u', // Hindi Devanagari
            'uk' => '/[а-яєіїґё]/ui',       // Ukrainian Cyrillic
            'bg' => '/[а-яьъ]/ui',         // Bulgarian Cyrillic
            'sr' => '/[а-яљњћџ]/ui',      // Serbian Cyrillic
            'el' => '/[α-ωάέήίόύώ]/ui',    // Greek
            'he' => '/[\x{0590}-\x{05ff}]/u', // Hebrew
        ];
        
        // Language similarity patterns to detect non-translations
        $similarity_check = [
            'es' => ['en' => 0.7],  // Spanish-English similarity threshold
            'pt' => ['es' => 0.8, 'en' => 0.6],  // Portuguese similarities
            'it' => ['es' => 0.7, 'en' => 0.6],  // Italian similarities
            'fr' => ['en' => 0.6],  // French-English
        ];
        
        foreach ($translations as $path => $translated_text) {
            if (empty($translated_text) || !is_string($translated_text)) {
                $issues[] = "Empty translation at path: $path";
                $suspicious_count++;
                continue;
            }
            
            // Check if translation is too similar to original (possible untranslated)
            // This would require original text comparison - skip for now
            
            // Check for appropriate character set
            if (isset($charset_patterns[$target_lang])) {
                $pattern = $charset_patterns[$target_lang];
                $clean_text = wp_strip_all_tags($translated_text);
                
                if (strlen($clean_text) > 3 && !preg_match($pattern, $clean_text)) {
                    $issues[] = "Translation may not be in target language ($target_lang) at path: $path";
                    $suspicious_count++;
                }
            }
            
            // Check for common translation failures
            if (strpos($translated_text, 'I cannot translate') !== false ||
                strpos($translated_text, 'Unable to translate') !== false ||
                strpos($translated_text, 'Error:') !== false ||
                strpos($translated_text, 'Sorry, I can\'t') !== false ||
                strpos($translated_text, 'Translation not available') !== false ||
                preg_match('/\b(error|failed|unable)\b/i', $translated_text)) {
                $issues[] = "Translation error response at path: $path";
                $suspicious_count++;
            }
            
            // Check for obvious non-translations (text too short after translation)
            $clean_original_estimate = strlen(wp_strip_all_tags($translated_text));
            if ($clean_original_estimate < 3 && strlen($translated_text) > 10) {
                $issues[] = "Suspiciously short translation at path: $path";
                $suspicious_count++;
            }
            
            // Check for HTML-only content (translations that lost all text)
            if (strlen(wp_strip_all_tags($translated_text)) === 0 && strlen($translated_text) > 0) {
                $issues[] = "Translation contains only HTML tags at path: $path";
                $suspicious_count++;
            }
        }
        
        // Overall quality check
        if ($total_count > 0) {
            $failure_rate = ($suspicious_count / $total_count) * 100;
            if ($failure_rate > 30) {
                $issues[] = "High failure rate: {$failure_rate}% of translations may be problematic";
            } elseif ($failure_rate > 10) {
                $issues[] = "Moderate failure rate: {$failure_rate}% of translations flagged for review";
            }
            
            // Log quality summary
            if (empty($issues)) {
                AITRFOEL_Logger::log("Translation quality: EXCELLENT (no issues detected)", 'success');
            } elseif ($failure_rate < 10) {
                AITRFOEL_Logger::log("Translation quality: GOOD (minor issues: {$failure_rate}%)", 'info');
            } elseif ($failure_rate < 30) {
                AITRFOEL_Logger::log("Translation quality: FAIR (issues: {$failure_rate}%)", 'warning');
            } else {
                AITRFOEL_Logger::log("Translation quality: POOR (major issues: {$failure_rate}%)", 'error');
            }
        }
        
        return $issues;
    }

    /**
     * Public test method for debugging HTML restoration
     */
    public function test_html_restoration($original_html, $translated_text = null) {
        AITRFOEL_Logger::log("=== HTML Restoration Test Started ===", 'debug');
        AITRFOEL_Logger::log("Original HTML: " . $original_html, 'debug');
        
        // Clean the text first
        $cleaned = $this->clean_text_for_translation($original_html);
        AITRFOEL_Logger::log("Cleaned text: " . $cleaned, 'debug');
        
        // If no translated text provided, use the cleaned text as mock translation
        if ($translated_text === null) {
            $translated_text = $cleaned;
        }
        
        AITRFOEL_Logger::log("Translated text: " . $translated_text, 'debug');
        
        // Restore HTML (now works with hash-based storage)
        $restored = $this->restore_html_formatting($translated_text, $original_html);
        AITRFOEL_Logger::log("Restored HTML: " . $restored, 'debug');
        AITRFOEL_Logger::log("=== HTML Restoration Test Ended ===", 'debug');
        
        return [
            'original' => $original_html,
            'cleaned' => $cleaned,
            'translated' => $translated_text,
            'restored' => $restored
        ];
    }

    /**
     * Track translation failure with analytics
     * 
     * @param string $error_code Error code
     * @param string $error_message Error message
     * @param float|null $start_time Translation start time for duration calculation
     */
    private function track_translation_failure($error_code, $error_message, $start_time = null) {
        if (!function_exists('aitrfoel_analytics')) {
            return;
        }
        
        $duration = $start_time ? round(microtime(true) - $start_time, 2) : 0;
        
        aitrfoel_analytics()->track_translation_failed($error_code, $error_message);
        aitrfoel_analytics()->track_translation_completed([
            'duration' => $duration,
            'word_count' => 0,
            'widget_count' => 0,
            'success' => false,
            'error_type' => $error_code,
            'mode' => 'single',
        ]);
    }

}
