<?php
// Exit if accessed directly
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Handle AJAX requests for the plugin
 *
 * @link       https://en.mementor.no/wordpress-plugins/text-to-speech-wordpress/
 * @since      1.0.0
 *
 * This class defines all code necessary to handle AJAX requests
 * including API key validation, voice retrieval, and text-to-speech conversion.
 *
 * @package    TTSWP
 * @subpackage MTTS/includes
 * @author     Mementor AS
 */
class Mementor_TTS_Ajax {
    /**
     * Instance of the class
     */
    private static $instance = null;

    /**
     * Constructor
     */
    private function __construct() {
        // Add AJAX actions
        add_action('wp_ajax_mementor_tts_generate_audio', array($this, 'generate_audio'));
        add_action('wp_ajax_mementor_tts_regenerate_audio', array($this, 'regenerate_audio'));
        add_action('wp_ajax_mementor_tts_delete_audio', array($this, 'delete_audio'));
        add_action('wp_ajax_mementor_tts_save_settings', array($this, 'save_settings'));
        add_action('wp_ajax_mementor_tts_test_api', array($this, 'test_api'));
        add_action('wp_ajax_mementor_tts_get_voices', array($this, 'get_voices'));
        add_action('wp_ajax_mementor_tts_clear_voice_cache', array($this, 'clear_voice_cache'));
        add_action('wp_ajax_mementor_tts_save_voice', array($this, 'save_voice'));
        add_action('wp_ajax_mementor_tts_clear_all_saved_voices', array($this, 'clear_all_saved_voices'));
        add_action('wp_ajax_mementor_tts_generate_shortcode_audio', array($this, 'ajax_generate_shortcode_audio'));
        add_action('wp_ajax_mementor_tts_debug_credits', array($this, 'debug_credits'));
        add_action('wp_ajax_mementor_tts_save_model', array($this, 'save_model'));
        add_action('wp_ajax_mementor_tts_refresh_stats', array($this, 'refresh_stats'));
        add_action('wp_ajax_mementor_tts_test_s3_connection', array($this, 'test_s3_connection'));

        // Speech to Text actions
        add_action('wp_ajax_mementor_stt_transcribe', array($this, 'transcribe_audio'));
        add_action('wp_ajax_mementor_stt_delete_transcription', array($this, 'delete_transcription'));

        // Public actions (need to work for non-logged-in users)
        add_action('wp_ajax_nopriv_mementor_tts_generate_shortcode_audio', array($this, 'ajax_generate_shortcode_audio'));

        // Review dismiss
        add_action('wp_ajax_mementor_tts_dismiss_review', array($this, 'handle_dismiss_review'));

        // Mark as reviewed (user clicked the review link)
        add_action('wp_ajax_mementor_tts_mark_reviewed', array($this, 'handle_mark_reviewed'));

        // Clear transients
        add_action('wp_ajax_mementor_tts_clear_transients', array($this, 'clear_transients'));

        // Rename audio file
        add_action('wp_ajax_mementor_tts_rename_audio', array($this, 'rename_audio'));

        // Bulk download zip
        add_action('wp_ajax_mementor_tts_bulk_download_zip', array($this, 'bulk_download_zip'));

        // Proxy download (avoids CORS for CloudFront/S3 URLs)
        add_action('wp_ajax_mementor_tts_proxy_download', array($this, 'proxy_download'));

        // Cleanup scheduled zip files
        add_action('mementor_tts_cleanup_zip', function ($path) {
            if (file_exists($path)) {
                wp_delete_file($path);
            }
        });
    }

    /**
     * Handle review dismiss AJAX request
     */
    public function handle_dismiss_review() {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array('message' => __('Invalid security token.', 'text-to-speech-tts')));
            return;
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied.', 'text-to-speech-tts')));
            return;
        }

        $type = isset($_POST['dismiss_type']) ? sanitize_text_field(wp_unslash($_POST['dismiss_type'])) : 'permanent';
        $uid  = get_current_user_id();

        if ( 'snooze' === $type ) {
            update_user_meta( $uid, 'ttswp_review_snoozed', time() );
        } else {
            update_user_meta( $uid, 'ttswp_review_dismissed', 'permanent' );
        }

        // Legacy compat
        update_option('mementor_tts_review_dismissed', 'yes', false);

        wp_send_json_success();
    }

    /**
     * Handle mark as reviewed AJAX request.
     *
     * @since 2.1.3
     */
    public function handle_mark_reviewed() {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array('message' => __('Invalid security token.', 'text-to-speech-tts')));
            return;
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied.', 'text-to-speech-tts')));
            return;
        }

        update_option('mementor_tts_has_reviewed', true, false);
        wp_send_json_success();
    }

    /**
     * Clear all plugin transients
     *
     * @since 2.1.0
     */
    public function clear_transients() {
        // Check nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array('message' => __('Invalid security token.', 'text-to-speech-tts')));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied.', 'text-to-speech-tts')));
            return;
        }

        global $wpdb;

        // Delete all transients matching our prefix (both value and timeout entries)
        $like_value = '%' . $wpdb->esc_like('_transient_mementor_tts_') . '%';
        $like_timeout = '%' . $wpdb->esc_like('_transient_timeout_mementor_tts_') . '%';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk transient cleanup.
        $count = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
                $like_value,
                $like_timeout
            )
        );

        $deleted = max(0, intval($count / 2)); // Each transient has a value + timeout row

        wp_send_json_success(array(
            'message' => sprintf(
                /* translators: %d: number of transients cleared */
                __('Cleared %d transient(s).', 'text-to-speech-tts'),
                $deleted
            )
        ));
    }

    /**
     * Rename an audio file from old mementor-{ID}-{lang}.mp3 format to {slug}-{ID}-{lang}.mp3
     */
    public function rename_audio() {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array('message' => __('Security check failed.', 'text-to-speech-tts')));
            return;
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied.', 'text-to-speech-tts')));
            return;
        }

        $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
        if (!$post_id) {
            wp_send_json_error(array('message' => __('Invalid post ID.', 'text-to-speech-tts')));
            return;
        }

        $upload_dir = wp_upload_dir();
        $audio_base_url = $upload_dir['baseurl'] . '/text-to-speech-tts/';
        $audio_base_dir = $upload_dir['basedir'] . '/text-to-speech-tts/';

        // Find all audio meta keys for this post
        $meta_keys = array('_mementor_tts_audio_url');
        $all_meta = get_post_meta($post_id);
        foreach (array_keys($all_meta) as $key) {
            if (strpos($key, '_mementor_tts_audio_url_') === 0) {
                $meta_keys[] = $key;
            }
        }

        $renamed = 0;
        $skipped = 0;
        $post_title = get_the_title($post_id);
        $slug = substr(sanitize_title($post_title), 0, 80);

        // Collect old_url => new_url mapping for all eligible files,
        // then rename files and update ALL meta keys at once to avoid
        // the first rename making the file disappear for subsequent keys.
        $old_url = $audio_base_url . 'mementor-' . $post_id . '-';
        $renames = array(); // old_filename => array( old_url, new_url, new_filename )

        foreach ($meta_keys as $meta_key) {
            $audio_url = get_post_meta($post_id, $meta_key, true);
            if (empty($audio_url)) {
                continue;
            }

            // Skip S3 URLs
            if (strpos($audio_url, 'amazonaws.com') !== false || strpos($audio_url, '.s3.') !== false) {
                $skipped++;
                continue;
            }

            // Only target files matching the old mementor-{ID}-{lang}.mp3 pattern
            $filename = basename($audio_url);
            if (!preg_match('/^mementor-(\d+)-([a-z]{2,5})\.mp3$/i', $filename, $matches)) {
                $skipped++;
                continue;
            }

            // Build rename entry (dedup by filename since multiple meta keys may reference the same file)
            if (!isset($renames[$filename])) {
                $file_post_id = intval($matches[1]);
                $lang_code = $matches[2];
                $new_filename = $slug . '-' . $file_post_id . '-' . $lang_code . '.mp3';
                $renames[$filename] = array(
                    'old_url'      => $audio_url,
                    'new_url'      => $audio_base_url . $new_filename,
                    'new_filename' => $new_filename,
                    'meta_keys'    => array($meta_key),
                );
            } else {
                $renames[$filename]['meta_keys'][] = $meta_key;
            }
        }

        // Now perform the actual file renames and meta updates
        foreach ($renames as $old_filename => $info) {
            $old_path = $audio_base_dir . $old_filename;
            $new_path = $audio_base_dir . $info['new_filename'];

            if (!file_exists($old_path) || file_exists($new_path)) {
                $skipped++;
                continue;
            }

            if (!rename($old_path, $new_path)) {
                $skipped++;
                continue;
            }

            $new_url_safe = esc_url_raw($info['new_url']);

            // Update ALL meta keys that referenced this file
            foreach ($info['meta_keys'] as $meta_key) {
                update_post_meta($post_id, $meta_key, $new_url_safe);
            }

            // Update Media Library attachment if one exists
            $attachment_id = get_post_meta($post_id, '_mementor_tts_attachment_id', true);
            if ($attachment_id) {
                $attachment_id = absint($attachment_id);
                if (get_post($attachment_id)) {
                    $old_attached = get_post_meta($attachment_id, '_wp_attached_file', true);
                    if ($old_attached) {
                        update_post_meta($attachment_id, '_wp_attached_file', str_replace($old_filename, $info['new_filename'], $old_attached));
                    }
                    wp_update_post(array(
                        'ID' => $attachment_id,
                        'guid' => $new_url_safe,
                    ));
                }
            }

            $renamed++;
        }

        if ($renamed > 0) {
            wp_send_json_success(array(
                'message' => sprintf(
                    /* translators: %d: number of files renamed */
                    __('Renamed %d file(s).', 'text-to-speech-tts'),
                    $renamed
                ),
                'renamed' => $renamed,
                'skipped' => $skipped,
            ));
        } else {
            wp_send_json_success(array(
                'message' => __('No files needed renaming.', 'text-to-speech-tts'),
                'renamed' => 0,
                'skipped' => $skipped,
            ));
        }
    }

    /**
     * Create a zip archive of selected audio files for bulk download
     */
    public function proxy_download() {
        if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_GET['nonce'])), 'mementor_tts_nonce')) {
            wp_die(__('Security check failed.', 'text-to-speech-tts'));
        }

        if (!current_user_can('edit_posts')) {
            wp_die(__('Unauthorized.', 'text-to-speech-tts'));
        }

        $url = isset($_GET['url']) ? esc_url_raw(wp_unslash($_GET['url'])) : '';
        if (empty($url)) {
            wp_die(__('No URL provided.', 'text-to-speech-tts'));
        }

        // Only allow downloads from trusted domains (CloudFront, S3, own site)
        $allowed_hosts = array(
            wp_parse_url(home_url(), PHP_URL_HOST),
        );

        // Add CloudFront/S3 domains from settings
        $cdn_url = get_option('mementor_tts_cdn_url', '');
        if ($cdn_url) {
            $allowed_hosts[] = wp_parse_url($cdn_url, PHP_URL_HOST);
        }
        $s3_bucket = get_option('mementor_tts_s3_bucket', '');
        if ($s3_bucket) {
            $allowed_hosts[] = $s3_bucket . '.s3.amazonaws.com';
        }

        // Allow CloudFront, S3, and TTSWP CDN domains
        $app_host = wp_parse_url(Mementor_TTS_Connect::get_app_url(), PHP_URL_HOST);
        if ($app_host) {
            // Allow app.ttswp.com and cdn.ttswp.com (any subdomain of the app domain's root)
            $root_domain = implode('.', array_slice(explode('.', $app_host), -2));
            $allowed_hosts[] = $app_host;
            $allowed_hosts[] = 'cdn.' . $root_domain;
        }

        $url_host = wp_parse_url($url, PHP_URL_HOST);
        $is_allowed = false;
        foreach ($allowed_hosts as $host) {
            if ($host && $url_host === $host) {
                $is_allowed = true;
                break;
            }
        }
        if (!$is_allowed) {
            wp_die(__('Download not allowed from this domain.', 'text-to-speech-tts'));
        }

        $response = wp_remote_get($url, array('timeout' => 30));
        if (is_wp_error($response)) {
            wp_die(__('Failed to fetch audio file.', 'text-to-speech-tts'));
        }

        $body = wp_remote_retrieve_body($response);
        $content_type = wp_remote_retrieve_header($response, 'content-type');

        // Strip CRLF to prevent header injection, validate against audio MIME allowlist
        $content_type = str_replace(array("\r", "\n"), '', $content_type);
        $allowed_audio_types = array('audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/wav', 'audio/x-wav', 'audio/aac');
        if (empty($content_type) || !in_array($content_type, $allowed_audio_types, true)) {
            $content_type = 'audio/mpeg';
        }

        $filename = isset($_GET['filename']) ? sanitize_file_name(wp_unslash($_GET['filename'])) : 'text-to-speech-generated.mp3';

        header('Content-Type: ' . $content_type);
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Length: ' . strlen($body));
        header('Cache-Control: no-cache, no-store, must-revalidate');
        echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        exit;
    }

    public function bulk_download_zip() {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array('message' => __('Security check failed.', 'text-to-speech-tts')));
            return;
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied.', 'text-to-speech-tts')));
            return;
        }

        $urls = isset($_POST['urls']) ? array_map('esc_url_raw', wp_unslash($_POST['urls'])) : array();
        if (empty($urls)) {
            wp_send_json_error(array('message' => __('No files selected.', 'text-to-speech-tts')));
            return;
        }

        if (!class_exists('ZipArchive')) {
            wp_send_json_error(array('message' => __('ZipArchive extension is not available on this server.', 'text-to-speech-tts')));
            return;
        }

        $upload_dir = wp_upload_dir();
        $zip_filename = 'tts-audio-' . gmdate('Y-m-d-His') . '.zip';
        $zip_path = $upload_dir['basedir'] . '/text-to-speech-tts/' . $zip_filename;
        $zip_url = $upload_dir['baseurl'] . '/text-to-speech-tts/' . $zip_filename;

        $zip = new ZipArchive();
        if ($zip->open($zip_path, ZipArchive::CREATE) !== true) {
            wp_send_json_error(array('message' => __('Could not create zip file.', 'text-to-speech-tts')));
            return;
        }

        $added = 0;
        $temp_files = array();
        foreach ($urls as $url) {
            // Local file
            $file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $url);
            if (strpos($url, $upload_dir['baseurl']) !== false && file_exists($file_path)) {
                $zip->addFile($file_path, basename($file_path));
                $added++;
            } else {
                // Remote/CDN file — download to temp
                $tmp = download_url($url, 30);
                if (!is_wp_error($tmp) && file_exists($tmp)) {
                    $zip->addFile($tmp, basename(wp_parse_url($url, PHP_URL_PATH)));
                    $temp_files[] = $tmp;
                    $added++;
                }
            }
        }

        $zip->close();

        // Clean up temp downloads
        foreach ($temp_files as $tmp) {
            wp_delete_file($tmp);
        }

        if ($added === 0) {
            wp_delete_file($zip_path);
            wp_send_json_error(array('message' => __('No valid files found to download.', 'text-to-speech-tts')));
            return;
        }

        // Schedule cleanup of the temp zip after 5 minutes
        wp_schedule_single_event(time() + 300, 'mementor_tts_cleanup_zip', array($zip_path));

        wp_send_json_success(array(
            'url' => $zip_url,
            'filename' => $zip_filename,
            'count' => $added,
        ));
    }

    /**
     * Get instance of the class
     */
    public static function get_instance() {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Generate audio via AJAX
     */
    public function generate_audio() {
        // Include required classes at the beginning
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-processor.php';
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-speech-builder.php';
        
        // Check nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('edit_posts')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Get and validate parameters
        $text = isset($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
        $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
        $voice_id = isset($_POST['voice_id']) ? sanitize_text_field(wp_unslash($_POST['voice_id'])) : '';

        ttswp_send_remote_debug_event(
            'generate_start',
            'generate_audio() invoked',
            array(
                'has_text_param' => ! empty( $text ),
                'text_len_param' => is_string( $text ) ? strlen( $text ) : 0,
                'voice_id'       => $voice_id,
            ),
            'debug',
            $post_id
        );

        // Check for custom voice ID if no voice_id provided and paid plan is active
        if (empty($voice_id)) {
            if (ttswp_has_paid_plan()) {
                $custom_voice_id = get_option('mementor_tts_custom_voice_id', '');
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Paid plan active, custom voice ID: ' . $custom_voice_id);
                }
                if (!empty($custom_voice_id)) {
                    $voice_id = $custom_voice_id;
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Using custom voice ID: ' . $voice_id);
                    }
                } else {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Custom voice ID is empty');
                    }
                }
            }
        } else {
            if (mementor_tts_is_debug_enabled()) {
                error_log('[TTS AJAX] Voice ID was provided in POST request');
            }
        }

        // Check for language-specific voice based on post language (WPML/Weglot)
        if (empty($voice_id) && class_exists('Mementor_TTS_I18n_Helper')) {
            $lang_voice_id = !empty($post_id) ? Mementor_TTS_I18n_Helper::get_voice_for_post($post_id) : Mementor_TTS_I18n_Helper::get_voice_for_language();
            if (!empty($lang_voice_id)) {
                $voice_id = $lang_voice_id;
                if (mementor_tts_is_debug_enabled()) {
                    $post_lang = !empty($post_id) ? Mementor_TTS_I18n_Helper::get_post_language($post_id) : Mementor_TTS_I18n_Helper::get_current_language();
                    error_log('[TTS AJAX] Using language-specific voice for ' . $post_lang . ': ' . $voice_id);
                }
            }
        }

        if (mementor_tts_is_debug_enabled()) {
            error_log('[TTS AJAX] Final voice_id to be used: ' . $voice_id);
        }

        // PRO feature: similarity, style, privacy mode
        $similarity = isset($_POST['mementor_tts_similarity']) ? floatval($_POST['mementor_tts_similarity']) : null;
        $style = isset($_POST['mementor_tts_style']) ? floatval($_POST['mementor_tts_style']) : null;
        $privacy_mode = isset($_POST['mementor_tts_privacy_mode']) ? (bool) $_POST['mementor_tts_privacy_mode'] : false;

        // Verify post existence
        if (empty($post_id) || !get_post($post_id)) {
            wp_send_json_error(array(
                'message' => __('Invalid post ID provided.', 'text-to-speech-tts')
            ));
            return;
        }

        // If no text provided, extract from post
        // For WooCommerce products, use product-specific assembly
        if (empty($text) && $post_id && get_post_type($post_id) === 'product' && function_exists('wc_get_product')) {
            $text = $this->assemble_product_text($post_id);
            // Ensure processor is available for later generate_audio call
            if (!empty($text) && !isset($processor)) {
                $processor = Mementor_TTS_Processor::get_instance();
            }
        }

        if (empty($text) && $post_id) {
            // Get the post content
            $post = get_post($post_id);
            if ($post) {
                // Include required classes
                require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-processor.php';
                require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-speech-builder.php';
                
                // Use the processor to extract text from the post
                if (!isset($processor)) {
                    $processor = Mementor_TTS_Processor::get_instance();
                }
                $speech_builder = Mementor_TTS_Speech_Builder::get_instance();
                
                // Check if this is a Fusion Builder post FIRST. Match both the
                // rendered class names (fusion-text / fusion-fullwidth) and
                // the raw shortcodes ([fusion_text / [fusion_builder_container)
                // — classic-editor Fusion posts save the latter, not the former.
                $is_fusion = strpos($post->post_content, 'fusion-text') !== false
                    || strpos($post->post_content, 'fusion-fullwidth') !== false
                    || strpos($post->post_content, '[fusion_text') !== false
                    || strpos($post->post_content, '[fusion_builder_container') !== false;
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Post ' . $post_id . ' - Is Fusion: ' . ($is_fusion ? 'YES' : 'NO'));
                }

                ttswp_send_remote_debug_event(
                    'content_extracted',
                    'Fusion detection',
                    array(
                        'is_fusion'    => (bool) $is_fusion,
                        'raw_post_len' => strlen( $post->post_content ),
                    ),
                    'debug',
                    $post_id
                );

                // Always re-extract content to respect current selectors
                $elements = array();

                // Force re-extraction for all posts to ensure blocked selectors are respected
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Re-extracting content to respect current selectors');
                }

                // Strip audio/video player shortcodes BEFORE applying filters
                $raw_content = $post->post_content;

                // Early detection: if post_content contains raw Elementor JSON data,
                // extract text from _elementor_data BEFORE it goes through the_content
                // filters (wptexturize mangles JSON quotes making it unrecoverable).
                $elementor_json_intercepted = false;
                if (strpos($raw_content, '"elType"') !== false || strpos($raw_content, "'elType'") !== false) {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] EARLY DETECTION: Raw Elementor JSON found in post_content - intercepting before the_content filters');
                    }
                    $elementor_data_json_early = get_post_meta($post_id, '_elementor_data', true);
                    if (!empty($elementor_data_json_early)) {
                        $elementor_data_early = json_decode($elementor_data_json_early, true);
                        if (is_array($elementor_data_early)) {
                            $elementor_text_early = $this->extract_text_from_elementor_data($elementor_data_early);
                            if (!empty($elementor_text_early)) {
                                $raw_content = $elementor_text_early;
                                $elementor_json_intercepted = true;
                                if (mementor_tts_is_debug_enabled()) {
                                    error_log('[TTS AJAX] Replaced raw Elementor JSON with extracted text, length: ' . strlen($elementor_text_early));
                                    error_log('[TTS AJAX] Extracted text preview: ' . substr(wp_strip_all_tags($elementor_text_early), 0, 200));
                                }
                            }
                        }
                    }
                }

                $audio_player_shortcodes = array('tp_audio_player', 'audio', 'video', 'embed', 'playlist', 'soundcloud', 'spotify', 'youtube', 'vimeo', 'mediaelementwrapper', 'fv_player', 'bp_media', 'powerpress');
                foreach ($audio_player_shortcodes as $shortcode) {
                    $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\]/i', '', $raw_content);
                    $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\].*?\[\/' . preg_quote($shortcode, '/') . '\]/is', '', $raw_content);
                }

                // Render through the_content in a faked singular context so page
                // builders (Fusion Builder, Avada, etc.) emit the full post body
                // and not just the teaser above <!--more-->. See
                // ttswp_render_content_full() for the full explanation.
                $rendered_content = ttswp_render_content_full($raw_content, $post_id);

                // Strip audio/video player HTML elements from rendered content
                $rendered_content = preg_replace('/<(audio|video)[^>]*>.*?<\/\1>/is', '', $rendered_content);
                $rendered_content = preg_replace('/<div[^>]*elementor-widget-tp-audio-player[^>]*>.*?<\/div>\s*<\/div>/is', '', $rendered_content);
                $rendered_content = preg_replace('/<div[^>]*data-widget_type="tp-audio-player[^"]*"[^>]*>.*?<\/div>\s*<\/div>/is', '', $rendered_content);
                $rendered_content = preg_replace('/<div[^>]*tp-audio-player-wrapper[^>]*>.*?<\/div>/is', '', $rendered_content);
                $rendered_content = preg_replace('/<div[^>]*class="[^"]*tp-player[^"]*"[^>]*>.*?<\/div>/is', '', $rendered_content);
                $audio_ui_classes = array('audio-player', 'video-player', 'media-player', 'tp-audio', 'plus-audio', 'theplus-audio', 'mejs-container', 'plyr', 'wp-audio-shortcode', 'wp-video-shortcode', 'elementor-widget-audio', 'elementor-widget-video', 'elementor-widget-tp-audio-player');
                foreach ($audio_ui_classes as $class_pattern) {
                    $rendered_content = preg_replace('/<div[^>]*class="[^"]*' . preg_quote($class_pattern, '/') . '[^"]*"[^>]*>.*?<\/div>/is', '', $rendered_content);
                }

                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Initial rendered content length: ' . strlen($rendered_content));
                }

                $ttswp_rendered_len = strlen( $rendered_content );
                $ttswp_raw_len      = strlen( $post->post_content );
                $ttswp_sample       = ( get_option( 'mementor_tts_remote_debug_include_content', '0' ) === '1' )
                    ? mb_substr( wp_strip_all_tags( $rendered_content ), 0, 400 )
                    : null;
                $ttswp_ctx = array(
                    'raw_post_len' => $ttswp_raw_len,
                    'rendered_len' => $ttswp_rendered_len,
                );
                if ( $ttswp_sample !== null ) {
                    $ttswp_ctx['rendered_sample'] = $ttswp_sample;
                }
                ttswp_send_remote_debug_event(
                    'content_extracted',
                    sprintf( 'Rendered content length: %d (raw %d)', $ttswp_rendered_len, $ttswp_raw_len ),
                    $ttswp_ctx,
                    $ttswp_rendered_len < 500 && $ttswp_raw_len > 1500 ? 'warning' : 'debug',
                    $post_id
                );

                // Fusion diagnostic: dump the raw rendered HTML shape so we can see
                // WHY the Fusion extractor drops most of the post. Only fires when
                // the site owner has opted in to content samples.
                if ( $is_fusion && get_option( 'mementor_tts_remote_debug_include_content', '0' ) === '1' ) {
                    $ttswp_ft_count = preg_match_all( '/<div[^>]*class="[^"]*fusion-text[^"]*"/i', $rendered_content, $ttswp_ft_m );
                    $ttswp_fc_count = preg_match_all( '/<div[^>]*class="[^"]*fusion-(?:builder-container|fullwidth)[^"]*"/i', $rendered_content, $ttswp_fc_m );
                    ttswp_send_remote_debug_event(
                        'fusion_diag',
                        sprintf( 'Fusion diag — fusion-text divs: %d, containers: %d', (int) $ttswp_ft_count, (int) $ttswp_fc_count ),
                        array(
                            'fusion_text_div_count'      => (int) $ttswp_ft_count,
                            'fusion_container_div_count' => (int) $ttswp_fc_count,
                            'rendered_len'               => $ttswp_rendered_len,
                            'rendered_html_head'         => mb_substr( $rendered_content, 0, 8000 ),
                            'rendered_html_tail'         => mb_substr( $rendered_content, -2000 ),
                        ),
                        'debug',
                        $post_id
                    );
                }

                // Debug: Check if Elementor is active
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Elementor class exists: ' . (class_exists('\Elementor\Plugin') ? 'YES' : 'NO'));
                }

                // For Elementor pages, we might need to get the full content differently
                if (class_exists('\Elementor\Plugin')) {
                    $is_built_with_elementor = \Elementor\Plugin::$instance->db->is_built_with_elementor($post_id);
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Post built with Elementor: ' . ($is_built_with_elementor ? 'YES' : 'NO'));
                    }

                    if ($is_built_with_elementor) {
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS AJAX] Elementor page detected, getting full Elementor content');
                        }
                        // Get Elementor content with all wrappers
                        $elementor = \Elementor\Plugin::$instance;
                        $with_css = false; // We don't need CSS
                        $elementor_content = $elementor->frontend->get_builder_content($post_id, $with_css);
                        if (!empty($elementor_content)) {
                            $rendered_content = $elementor_content;
                            $rendered_content = $elementor_content;
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Using Elementor builder content, length: ' . strlen($rendered_content));
                                error_log('[TTS AJAX] Elementor content preview: ' . substr($rendered_content, 0, 500) . '...');
                            }
                        } else {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Elementor builder content was empty');
                            }
                        }
                    }
                } else {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Elementor plugin not active or class not loaded');
                    }
                }

                // If we still don't have the full HTML structure, try getting raw page content
                if (strpos($rendered_content, 'no-audio') === false && strpos($rendered_content, 'elementor-element') === false) {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Rendered content missing Elementor structure, trying alternative method');
                    }

                    // Try to fetch the actual page HTML via HTTP request
                    $page_url = get_permalink($post_id);
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Attempting to fetch full page HTML from: ' . $page_url);
                    }
                    
                    // Try to get Elementor data directly from post meta
                    $elementor_data_json = get_post_meta($post_id, '_elementor_data', true);
                    if (!empty($elementor_data_json)) {
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS AJAX] Found Elementor data in post meta, attempting to parse');
                        }

                        // Decode the JSON data
                        $elementor_data = json_decode($elementor_data_json, true);
                        if (is_array($elementor_data)) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Successfully parsed Elementor data');
                            }

                            // Look for elements with no-audio class
                            $has_no_audio = false;
                            array_walk_recursive($elementor_data, function($value, $key) use (&$has_no_audio) {
                                if ($key === 'css_classes' && strpos($value, 'no-audio') !== false) {
                                    $has_no_audio = true;
                                    if (mementor_tts_is_debug_enabled()) {
                                        error_log('[TTS AJAX] Found no-audio class in Elementor element settings: ' . $value);
                                    }
                                }
                            });

                            if ($has_no_audio) {
                                if (mementor_tts_is_debug_enabled()) {
                                    error_log('[TTS AJAX] Elementor data contains elements with no-audio class');
                                }
                            }
                        }
                    }

                    // Skip the HTTP request for now - it might be causing issues
                    $skip_http_request = true;
                    if ($skip_http_request) {
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS AJAX] Skipping HTTP request');
                        }
                    } else {
                        $response = wp_remote_get($page_url, array(
                            'timeout' => 10,
                            'sslverify' => false
                        ));
                    
                        if (!is_wp_error($response)) {
                        $page_html = wp_remote_retrieve_body($response);
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS AJAX] Fetched page HTML, length: ' . strlen($page_html));
                        }

                        // Debug: Check if the fetched HTML contains no-audio class
                        if (preg_match('/class=["\'][^"\']*\bno-audio\b[^"\']*["\']/i', $page_html)) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] SUCCESS: Full page HTML contains no-audio class');
                                // Find and log some examples
                                preg_match_all('/class=["\'][^"\']*\bno-audio\b[^"\']*["\']/i', $page_html, $class_matches);
                                foreach (array_slice($class_matches[0], 0, 3) as $match) {
                                    error_log('[TTS AJAX] Found no-audio in: ' . $match);
                                }
                            }
                        } else if (strpos($page_html, 'no-audio') !== false) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Full page HTML contains "no-audio" text but not as a class attribute');
                                // Log a sample of where no-audio appears
                                preg_match_all('/[^>]*no-audio[^<]*/i', $page_html, $matches);
                                foreach (array_slice($matches[0], 0, 3) as $match) {
                                    error_log('[TTS AJAX] Sample no-audio occurrence: ' . $match);
                                }
                            }
                        } else {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] WARNING: Full page HTML does not contain no-audio at all');
                            }
                        }

                        // Extract Elementor content more reliably
                        // First check if page has Elementor content
                        if (strpos($page_html, 'elementor') !== false) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Page contains Elementor content');
                            }
                            
                            // Try multiple patterns to extract the content
                            $patterns = array(
                                // Pattern 1: data-elementor-type="wp-page"
                                '#<div[^>]+data-elementor-type="wp-page"[^>]*>.*?</div>\s*<!--\s*\.elementor\s*-->#s',
                                // Pattern 2: class containing elementor
                                '#<div[^>]+class="[^"]*elementor[^"]*"[^>]*data-elementor-id="\d+"[^>]*>.*?</div>\s*<!--[^>]+elementor[^>]+-->#s',
                                // Pattern 3: More general elementor container
                                '#<div[^>]+class="[^"]*elementor[^"]*"[^>]*>.*?</div>\s*<!--[^>]+-->#s',
                                // Pattern 4: Look for article content with elementor
                                '#<article[^>]*>.*?<div[^>]+class="[^"]*elementor[^"]*"[^>]*>.*?</div>.*?</article>#s'
                            );
                            
                            $elementor_content = '';
                            foreach ($patterns as $i => $pattern) {
                                if (preg_match($pattern, $page_html, $matches)) {
                                    if (mementor_tts_is_debug_enabled()) {
                                        error_log('[TTS AJAX] Pattern ' . ($i + 1) . ' matched, extracted content length: ' . strlen($matches[0]));
                                    }
                                    $elementor_content = $matches[0];
                                    break;
                                }
                            }

                            if (!empty($elementor_content)) {
                                if (mementor_tts_is_debug_enabled()) {
                                    error_log('[TTS AJAX] Extracted Elementor content, length: ' . strlen($elementor_content));
                                }

                                // Check for no-audio class or id
                                if (strpos($elementor_content, 'no-audio') !== false || strpos($elementor_content, 'id="no-audio"') !== false) {
                                    if (mementor_tts_is_debug_enabled()) {
                                        error_log('[TTS AJAX] SUCCESS: Found no-audio in extracted Elementor content');
                                    }
                                    $rendered_content = $elementor_content;
                                } else {
                                    if (mementor_tts_is_debug_enabled()) {
                                        error_log('[TTS AJAX] WARNING: Extracted content but no no-audio found');
                                    }
                                    // Use it anyway if it's larger than what we have
                                    if (strlen($elementor_content) > strlen($rendered_content) * 2) {
                                        if (mementor_tts_is_debug_enabled()) {
                                            error_log('[TTS AJAX] Using extracted content anyway as it\'s much larger');
                                        }
                                        $rendered_content = $elementor_content;
                                    }
                                }
                            } else {
                                if (mementor_tts_is_debug_enabled()) {
                                    error_log('[TTS AJAX] Could not extract Elementor content with any pattern');
                                }

                                // Last resort: extract everything between entry-content
                                if (preg_match('#<div[^>]+class="[^"]*entry-content[^"]*"[^>]*>(.*?)</div>#s', $page_html, $matches)) {
                                    if (mementor_tts_is_debug_enabled()) {
                                        error_log('[TTS AJAX] Extracted entry-content as fallback, length: ' . strlen($matches[1]));
                                    }
                                    $rendered_content = $matches[1];
                                }
                            }
                        } else {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Page does not contain Elementor markers');
                            }
                        }
                        } else {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Failed to fetch page HTML: ' . $response->get_error_message());
                            }
                        }
                    }
                    
                    // Try to get the full page content with a custom approach
                    global $wp_query;
                    $original_query = $wp_query;
                    
                    // Create a new query for just this post
                    $wp_query = new WP_Query(array(
                        'p' => $post_id,
                        'post_type' => 'any'
                    ));
                    
                    if ($wp_query->have_posts()) {
                        $wp_query->the_post();
                        
                        // Remove any filters that might strip HTML
                        $priority = has_filter('the_content', 'wpautop');
                        if ($priority !== false) {
                            remove_filter('the_content', 'wpautop', $priority);
                        }
                        
                        // Get the raw content with minimal filtering
                        ob_start();
                        
                        // Try to get Elementor's frontend output directly
                        if (class_exists('\Elementor\Plugin') && \Elementor\Plugin::$instance->preview->is_preview_mode()) {
                            // In preview mode
                            echo \Elementor\Plugin::$instance->preview->builder_wrapper('');
                        } else {
                            // Normal mode - get the content without stripping tags
                            global $post;
                            $content = $post->post_content;
                            
                            // Check if this is Elementor content
                            if (class_exists('\Elementor\Plugin') && \Elementor\Plugin::$instance->db->is_built_with_elementor($post_id)) {
                                // Get Elementor's raw data
                                $document = \Elementor\Plugin::$instance->documents->get($post_id);
                                if ($document) {
                                    $content = $document->get_content();
                                }
                            }
                            
                            echo $content;
                        }
                        
                        $full_content = ob_get_clean();
                        
                        // Re-add the filter if it was removed
                        if ($priority !== false) {
                            add_filter('the_content', 'wpautop', $priority);
                        }
                        
                        if (!empty($full_content) && strlen($full_content) > strlen($rendered_content)) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] Using alternative content extraction, length: ' . strlen($full_content));
                            }
                            $rendered_content = $full_content;

                            // Check if we now have the no-audio class
                            if (strpos($rendered_content, 'no-audio') !== false) {
                                if (mementor_tts_is_debug_enabled()) {
                                    error_log('[TTS AJAX] SUCCESS: Found no-audio class in alternative extraction');
                                }
                            }
                        }
                    }

                    // Restore original query
                    $wp_query = $original_query;
                    wp_reset_postdata();
                }

                // Safety check: detect raw Elementor JSON data in rendered content.
                // This happens when Elementor's frontend rendering fails in AJAX context,
                // leaving raw JSON (possibly with wptexturize-mangled quotes) in the content.
                // "elType" is an internal Elementor JSON key that never appears in normal
                // rendered HTML content - its presence means raw JSON leaked through.
                if (strpos($rendered_content, 'elType') !== false) {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] WARNING: Detected raw Elementor JSON in rendered content - extracting text from _elementor_data');
                    }

                    $elementor_data_json = get_post_meta($post_id, '_elementor_data', true);
                    if (!empty($elementor_data_json)) {
                        $elementor_data = json_decode($elementor_data_json, true);
                        if (is_array($elementor_data)) {
                            $elementor_text = $this->extract_text_from_elementor_data($elementor_data);
                            if (!empty($elementor_text)) {
                                // Wrap in proper HTML so the processor can handle it with selectors
                                $rendered_content = '<div class="post-content elementor">' . $elementor_text . '</div>';
                                if (mementor_tts_is_debug_enabled()) {
                                    error_log('[TTS AJAX] Extracted text from Elementor data, length: ' . strlen($elementor_text));
                                }
                            }
                        }
                    }
                }

                // Extract content using current selectors
                $extracted_content = $processor->get_content_from_html($rendered_content);

                if (!empty($extracted_content)) {
                    // Create a speech element object
                    $element = new stdClass();
                    $element->content = $extracted_content;
                    $element->post_id = $post_id;
                    $element->element_uid = 'post_content_' . $post_id;
                    $elements = array($element);
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Re-extracted content, length: ' . strlen($extracted_content));
                        error_log('[TTS AJAX] Re-extracted content preview: ' . substr($extracted_content, -200));
                    }
                } else {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] No content extracted');
                    }
                }

                // Build the complete content including title, author, excerpt based on settings
                $text = '';

                // Add title if enabled
                $include_title = get_option('mementor_tts_include_title', true);
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Include title setting: ' . ($include_title ? 'yes' : 'no'));
                }
                if ($include_title) {
                    $title = get_the_title($post_id);
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Adding title: ' . $title);
                    }
                    $text .= $title;

                    // Add author if enabled
                    $include_author = get_option('mementor_tts_include_author', false);
                    if ($include_author && $post->post_author) {
                        // First check for custom field 'author'
                        $author = get_post_meta($post_id, 'author', true);

                        // If no custom field, fall back to WordPress author
                        if (empty($author)) {
                            $author = get_the_author_meta('display_name', $post->post_author);
                        }

                        if (!empty($author)) {
                            $text .= ' -- ' . __('By', 'text-to-speech-tts') . ' ' . $author;
                        }
                    }

                    // Add date if enabled
                    $include_date = get_option('mementor_tts_include_date', false);
                    if ($include_date) {
                        $text .= ' -- ' . __('Published on', 'text-to-speech-tts') . ' ' . get_the_date('', $post_id);
                    }

                    // Add custom pause after title (paid plan feature)
                    if (ttswp_has_paid_plan()) {
                        $pause_duration = floatval(get_option('mementor_tts_pause_after_title', 0.6));
                    } else {
                        $pause_duration = 0.6;
                    }

                    $text .= ' <break time="' . $pause_duration . 's" /> ';
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Added pause after title: ' . $pause_duration . 's' . (ttswp_has_paid_plan() ? ' (paid)' : ' (default)'));
                    }
                }

                // Add excerpt if enabled
                $include_excerpt = get_option('mementor_tts_include_excerpt', false);
                if ($include_excerpt && has_excerpt($post_id)) {
                    $excerpt = get_the_excerpt($post);
                    if (!empty($excerpt)) {
                        $text .= wp_strip_all_tags($excerpt) . ' -- ';
                    }
                }

                // Now add the main content
                if (!empty($elements)) {
                    // Use elements if available (non-Fusion posts with saved elements)
                    $element_content = '';
                    foreach ($elements as $element) {
                        if (!empty($element->content)) {
                            $element_content .= $element->content . ' -- ';
                        }
                    }
                    $text .= rtrim($element_content, ' -- ');
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Using element content for non-Fusion post, length: ' . strlen($element_content));
                    }
                } else {
                    // Process content using CSS selectors if available
                    if (method_exists($processor, 'get_content_from_html')) {
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS AJAX] TEST MARKER V3 - Using get_content_from_html method');
                        }

                        // Strip audio/video player shortcodes BEFORE applying filters
                        $raw_content = $post->post_content;

                        // Early detection: intercept raw Elementor JSON before the_content filters
                        if (strpos($raw_content, '"elType"') !== false) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] EARLY DETECTION (fallback path): Raw Elementor JSON in post_content');
                            }
                            $ed_json = get_post_meta($post_id, '_elementor_data', true);
                            if (!empty($ed_json)) {
                                $ed_data = json_decode($ed_json, true);
                                if (is_array($ed_data)) {
                                    $ed_text = $this->extract_text_from_elementor_data($ed_data);
                                    if (!empty($ed_text)) {
                                        $raw_content = $ed_text;
                                    }
                                }
                            }
                        }

                        $audio_player_shortcodes = array('tp_audio_player', 'audio', 'video', 'embed', 'playlist', 'soundcloud', 'spotify', 'youtube', 'vimeo', 'mediaelementwrapper', 'fv_player', 'bp_media', 'powerpress');
                        foreach ($audio_player_shortcodes as $shortcode) {
                            $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\]/i', '', $raw_content);
                            $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\].*?\[\/' . preg_quote($shortcode, '/') . '\]/is', '', $raw_content);
                        }

                        // Render via helper so Fusion Builder / Avada emit the full
                        // post body in AJAX context, not just the teaser.
                        $rendered_content = ttswp_render_content_full($raw_content, $post_id);

                        // Strip audio/video player HTML elements from rendered content
                        $rendered_content = preg_replace('/<(audio|video)[^>]*>.*?<\/\1>/is', '', $rendered_content);
                        $rendered_content = preg_replace('/<div[^>]*elementor-widget-tp-audio-player[^>]*>.*?<\/div>\s*<\/div>/is', '', $rendered_content);
                        $rendered_content = preg_replace('/<div[^>]*data-widget_type="tp-audio-player[^"]*"[^>]*>.*?<\/div>\s*<\/div>/is', '', $rendered_content);
                        $rendered_content = preg_replace('/<div[^>]*tp-audio-player-wrapper[^>]*>.*?<\/div>/is', '', $rendered_content);
                        $rendered_content = preg_replace('/<div[^>]*class="[^"]*tp-player[^"]*"[^>]*>.*?<\/div>/is', '', $rendered_content);
                        $audio_ui_classes = array('audio-player', 'video-player', 'media-player', 'tp-audio', 'plus-audio', 'theplus-audio', 'mejs-container', 'plyr', 'wp-audio-shortcode', 'wp-video-shortcode', 'elementor-widget-audio', 'elementor-widget-video', 'elementor-widget-tp-audio-player');
                        foreach ($audio_ui_classes as $class_pattern) {
                            $rendered_content = preg_replace('/<div[^>]*class="[^"]*' . preg_quote($class_pattern, '/') . '[^"]*"[^>]*>.*?<\/div>/is', '', $rendered_content);
                        }

                        // Debug logging for Fusion Builder
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS Debug] Original content length: ' . strlen($post->post_content));
                            error_log('[TTS Debug] Rendered content length: ' . strlen($rendered_content));
                            
                            // Check for Fusion Builder markers
                            if (strpos($rendered_content, 'fusion-text') !== false) {
                                error_log('[TTS Debug] Fusion Builder content detected');
                                preg_match_all('/<div[^>]*class="[^"]*fusion-text[^"]*"[^>]*>/i', $rendered_content, $matches);
                                error_log('[TTS Debug] Found ' . count($matches[0]) . ' fusion-text elements');
                            }
                        }
                        
                        // Safety check: detect raw Elementor JSON in rendered content (fallback path)
                        if (strpos($rendered_content, 'elType') !== false) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] WARNING: Detected raw Elementor JSON in fallback rendered content');
                            }
                            $elementor_data_json_fb = get_post_meta($post_id, '_elementor_data', true);
                            if (!empty($elementor_data_json_fb)) {
                                $elementor_data_fb = json_decode($elementor_data_json_fb, true);
                                if (is_array($elementor_data_fb)) {
                                    $elementor_text_fb = $this->extract_text_from_elementor_data($elementor_data_fb);
                                    if (!empty($elementor_text_fb)) {
                                        $rendered_content = '<div class="post-content elementor">' . $elementor_text_fb . '</div>';
                                    }
                                }
                            }
                        }

                        // Let the processor handle all content extraction, including Fusion Builder
                        $content = $processor->get_content_from_html($rendered_content);
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS AJAX] Extracted content length: ' . strlen($content) . ' chars');

                            // Debug: Log first 200 chars of extracted content
                            error_log('[TTS AJAX] Extracted content preview: ' . substr($content, 0, 200) . '...');

                            // Count paragraphs for Fusion posts
                            if ($is_fusion) {
                                $paragraphs = explode(' -- ', $content);
                                $paragraph_count = count(array_filter($paragraphs, function($p) { return strlen(trim($p)) > 5; }));
                                error_log('[TTS AJAX] Fusion post has ' . $paragraph_count . ' paragraphs');
                            }
                        }
                        
                        if (mementor_tts_is_debug_enabled()) {
                            error_log('[TTS Debug] Extracted content length: ' . strlen($content));
                            error_log('[TTS Debug] First 200 chars: ' . substr($content, 0, 200));
                        }
                    } else {
                        // Fallback to simple tag stripping
                        // Strip audio/video player shortcodes BEFORE applying filters
                        $raw_content = $post->post_content;

                        // Early detection: intercept raw Elementor JSON
                        if (strpos($raw_content, '"elType"') !== false) {
                            $ed_json2 = get_post_meta($post_id, '_elementor_data', true);
                            if (!empty($ed_json2)) {
                                $ed_data2 = json_decode($ed_json2, true);
                                if (is_array($ed_data2)) {
                                    $ed_text2 = $this->extract_text_from_elementor_data($ed_data2);
                                    if (!empty($ed_text2)) {
                                        $raw_content = $ed_text2;
                                    }
                                }
                            }
                        }

                        $audio_player_shortcodes = array('tp_audio_player', 'audio', 'video', 'embed', 'playlist', 'soundcloud', 'spotify', 'youtube', 'vimeo', 'mediaelementwrapper', 'fv_player', 'bp_media', 'powerpress');
                        foreach ($audio_player_shortcodes as $shortcode) {
                            $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\]/i', '', $raw_content);
                            $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\].*?\[\/' . preg_quote($shortcode, '/') . '\]/is', '', $raw_content);
                        }

                        $content = ttswp_render_content_full($raw_content, $post_id);

                        // Safety check: detect raw Elementor JSON in content (simple fallback path)
                        if (strpos($content, 'elType') !== false) {
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('[TTS AJAX] WARNING: Detected raw Elementor JSON in simple fallback content');
                            }
                            $elementor_data_json_sf = get_post_meta($post_id, '_elementor_data', true);
                            if (!empty($elementor_data_json_sf)) {
                                $elementor_data_sf = json_decode($elementor_data_json_sf, true);
                                if (is_array($elementor_data_sf)) {
                                    $elementor_text_sf = $this->extract_text_from_elementor_data($elementor_data_sf);
                                    if (!empty($elementor_text_sf)) {
                                        $content = $elementor_text_sf;
                                    }
                                }
                            }
                        }

                        // Strip audio/video player HTML elements before stripping all tags
                        $content = preg_replace('/<(audio|video)[^>]*>.*?<\/\1>/is', '', $content);
                        $content = preg_replace('/<div[^>]*elementor-widget-tp-audio-player[^>]*>.*?<\/div>\s*<\/div>/is', '', $content);
                        $content = preg_replace('/<div[^>]*data-widget_type="tp-audio-player[^"]*"[^>]*>.*?<\/div>\s*<\/div>/is', '', $content);
                        $content = preg_replace('/<div[^>]*tp-audio-player-wrapper[^>]*>.*?<\/div>/is', '', $content);
                        $content = preg_replace('/<div[^>]*class="[^"]*tp-player[^"]*"[^>]*>.*?<\/div>/is', '', $content);
                        $audio_ui_classes = array('audio-player', 'video-player', 'media-player', 'tp-audio', 'plus-audio', 'theplus-audio', 'mejs-container', 'plyr', 'wp-audio-shortcode', 'wp-video-shortcode', 'elementor-widget-audio', 'elementor-widget-video', 'elementor-widget-tp-audio-player');
                        foreach ($audio_ui_classes as $class_pattern) {
                            $content = preg_replace('/<div[^>]*class="[^"]*' . preg_quote($class_pattern, '/') . '[^"]*"[^>]*>.*?<\/div>/is', '', $content);
                        }

                        if (function_exists('ttswp_inject_speech_breaks')) {
                            $content = ttswp_inject_speech_breaks($content);
                        }
                        $content = wp_strip_all_tags($content);
                    }

                    $text .= $content;
                }
                
                // Clean up the text and check if it's actually empty
                $text = trim($text);
                $text = trim($text, ' -');
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Text after content extraction: ' . $text);
                }

                // If still empty after all attempts, use just the title
                if (empty($text) || $text === '--') {
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('[TTS AJAX] Text was empty, using title as fallback');
                    }
                    $text = get_the_title($post_id);
                }

                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Final text before sending to processor: ' . $text);
                }

                // Note: Custom intro/outro is handled by the processor's generate_audio method
                // to avoid duplication. Do not add intro/outro here.
            }
        }
        
        if (empty($text)) {
            wp_send_json_error(array(
                'message' => __('No text content found for conversion.', 'text-to-speech-tts')
            ));
            return;
        }
        
        ttswp_send_remote_debug_event(
            'content_extracted',
            sprintf( 'Final text length before TTS call: %d', strlen( $text ) ),
            array( 'final_text_len' => strlen( $text ) ),
            strlen( $text ) < 50 ? 'warning' : 'debug',
            $post_id
        );

        // Debug logging before processing
        if (mementor_tts_is_debug_enabled()) {
            error_log('[TTS AJAX] Text length before generate_audio: ' . strlen($text) . ' chars');
            if ($is_fusion) {
                $debug_paragraphs = explode(' -- ', $text);
                $debug_paragraph_count = count(array_filter($debug_paragraphs, function($p) { return strlen(trim($p)) > 5; }));
                error_log('[TTS AJAX] Text has ' . $debug_paragraph_count . ' paragraphs before sending to generate_audio');
            }
        }
        
        // Note: Hyphen replacement is handled in the processor's process_text method
        // to ensure correct order of operations (double dashes first, then single hyphens)
        
        // Generate audio via SaaS API
        if (mementor_tts_is_debug_enabled()) {
            error_log('[TTS AJAX] Calling processor->generate_audio with voice_id: ' . $voice_id);
        }
        try {
            $result = $processor->generate_audio($text, $post_id, $voice_id);
        } catch (\Throwable $e) {
            error_log('[TTS AJAX] FATAL in generate_audio: ' . $e->getMessage() . ' in ' . basename($e->getFile()) . ':' . $e->getLine());
            ttswp_send_remote_debug_event(
                'generate_error',
                'FATAL in generate_audio: ' . $e->getMessage(),
                array(
                    'file' => basename( $e->getFile() ),
                    'line' => $e->getLine(),
                ),
                'error',
                $post_id
            );
            $result = array('success' => false, 'error' => $e->getMessage());
        }

        // Handle the result - generate_audio returns various types
        if ($result === false) {
            wp_send_json_error(array(
                'message' => __('Failed to generate audio. Please check your settings and try again.', 'text-to-speech-tts')
            ));
            return;
        } elseif (is_array($result) && isset($result['success']) && $result['success'] === false) {
            // Handle error responses that return as arrays
            $error_message = isset($result['message']) ? $result['message'] : (isset($result['error']) ? $result['error'] : __('Failed to generate audio.', 'text-to-speech-tts'));
            wp_send_json_error(array(
                'message' => $error_message,
                'error' => isset($result['error']) ? $result['error'] : null,
                'error_type' => isset($result['error_type']) ? $result['error_type'] : null,
                'credits_info' => isset($result['credits_info']) ? $result['credits_info'] : null
            ));
            return;
        } elseif (is_string($result)) {
            // Success - $result contains the file URL

            // Log credit usage for tracking
            $speech_builder = Mementor_TTS_Speech_Builder::get_instance();

            // Determine API type
            $api_type = 'shared'; // default
            $api_key = get_option('mementor_tts_api_key');
            if (ttswp_has_paid_plan()) {
                $api_type = 'saas';
            } elseif (!empty($api_key)) {
                $api_type = 'user';
            }

            // Get model ID from settings
            $model_id = get_option('mementor_tts_model', 'eleven_flash_v2_5');

            // Get actual credit cost from ElevenLabs (stored by processor after API call)
            $character_cost = get_transient('mementor_tts_char_cost_' . $post_id);
            if ($character_cost !== false && intval($character_cost) > 0) {
                $credits_used = intval($character_cost);
                delete_transient('mementor_tts_char_cost_' . $post_id);
            } else {
                $credits_used = !empty($text) ? strlen($text) : 0;
            }
            $text_length = !empty($text) ? strlen($text) : 0;

            // Log the generation
            if (mementor_tts_is_debug_enabled()) {
                error_log('[TTS AJAX] About to log credit usage: post_id=' . $post_id . ', credits_used=' . $credits_used . ', text_length=' . $text_length . ', api_type=' . $api_type);
            }
            $speech_builder->log_generation($post_id, $credits_used, array(
                'text_length' => $text_length,
                'voice_id'    => $voice_id,
                'model_id'    => $model_id,
                'api_type'    => $api_type,
                'file_size'   => 0,
            ));

            // Check if S3 upload should happen but didn't (fallback for opcache issues)
            $offload_enabled = get_option('mementor_tts_offload_enabled', 'no') === 'yes';
            $offload_provider = get_option('mementor_tts_offload_provider', 'local');
            error_log('[TTS AJAX] S3 Check - Offload enabled: ' . ($offload_enabled ? 'YES' : 'NO') . ', Provider: ' . $offload_provider);

            if ($offload_enabled && $offload_provider === 's3' && strpos($result, 's3.') === false && strpos($result, 'amazonaws.com') === false) {
                // S3 is enabled but URL is local - try to upload now
                error_log('[TTS AJAX] S3 enabled but got local URL, attempting S3 upload...');

                // Get the local file path from the URL
                $upload_dir = wp_upload_dir();
                $local_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $result);

                if (file_exists($local_path)) {
                    require_once MEMENTOR_TTS_PLUGIN_DIR . 'includes/class-mementor-tts-s3-handler.php';
                    $s3_handler = new Mementor_TTS_S3_Handler();

                    if ($s3_handler->is_configured()) {
                        $filename = basename($local_path);
                        $s3_result = $s3_handler->upload_file($local_path, $filename);

                        if (!is_wp_error($s3_result) && isset($s3_result['url'])) {
                            $result = $s3_result['url'];
                            error_log('[TTS AJAX] S3 upload successful: ' . $result);

                            // Update post meta with S3 URL
                            update_post_meta($post_id, '_mementor_tts_audio_url', esc_url_raw($result));

                            // Delete local file
                            wp_delete_file($local_path);
                            error_log('[TTS AJAX] Local file deleted after S3 upload');
                        } else {
                            $error = is_wp_error($s3_result) ? $s3_result->get_error_message() : 'Unknown error';
                            error_log('[TTS AJAX] S3 upload failed: ' . $error);
                        }
                    } else {
                        error_log('[TTS AJAX] S3 handler not configured');
                    }
                }
            }

        ttswp_send_remote_debug_event(
            'generate_success',
            sprintf( 'Generation OK — %d chars', strlen( $text ) ),
            array( 'text_len' => strlen( $text ) ),
            'info',
            $post_id
        );

            wp_send_json_success(array(
                'file_url' => $result,
                'message' => __('Audio generated successfully.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Fallback for unexpected result types
        wp_send_json_error(array(
            'message' => __('Unexpected response from audio generation.', 'text-to-speech-tts')
        ));
    }

    /**
     * Regenerate audio via AJAX
     */
    public function regenerate_audio() {
        // Check nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('edit_posts')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Get and validate parameters with proper sanitization
        $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
        $element_uid = isset($_POST['element_uid']) ? sanitize_key(wp_unslash($_POST['element_uid'])) : '';

        ttswp_send_remote_debug_event(
            'regenerate_start',
            'regenerate_audio() invoked',
            array( 'element_uid' => isset( $_POST['element_uid'] ) ? sanitize_text_field( wp_unslash( $_POST['element_uid'] ) ) : '' ),
            'debug',
            $post_id
        );

        // Validate post ID
        if (empty($post_id) || !get_post($post_id)) {
            wp_send_json_error(array(
                'message' => __('Invalid post ID provided.', 'text-to-speech-tts')
            ));
            return;
        }

        // Validate element UID
        if (empty($element_uid)) {
            wp_send_json_error(array(
                'message' => __('Missing element identifier.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Include required classes
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-processor.php';
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-speech-builder.php';

        // Get processor instance
        $processor = Mementor_TTS_Processor::get_instance();
        
        // Get speech builder instance
        $speech_builder = Mementor_TTS_Speech_Builder::get_instance();
        
        // Get element content
        $element = $speech_builder->get_element($post_id, $element_uid);
        if (!$element) {
            wp_send_json_error(array(
                'message' => __('Element not found. Cannot regenerate audio.', 'text-to-speech-tts')
            ));
            return;
        }

        // Get voice ID - use custom voice if available
        $voice_id = $element->voice_id;
        if (empty($voice_id) && ttswp_has_paid_plan()) {
            $custom_voice_id = get_option('mementor_tts_custom_voice_id', '');
            if (!empty($custom_voice_id)) {
                $voice_id = $custom_voice_id;
            }
        }
        
        // Always re-extract content on regenerate so current settings
        // (Truncate After, Include/Exclude Selectors, no-audio classes, etc.)
        // take effect. Previously only Fusion Builder posts re-extracted; all
        // other post types silently reused `$element->content` captured at
        // first generation and ignored later settings changes.
        $content_to_use = $element->content;
        $post = get_post($post_id);
        if ($post) {
            // Strip audio/video player shortcodes BEFORE applying filters
            $raw_content = $post->post_content;
            $audio_player_shortcodes = array('tp_audio_player', 'audio', 'video', 'embed', 'playlist', 'soundcloud', 'spotify', 'youtube', 'vimeo', 'mediaelementwrapper', 'fv_player', 'bp_media', 'powerpress');
            foreach ($audio_player_shortcodes as $shortcode) {
                $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\]/i', '', $raw_content);
                $raw_content = preg_replace('/\[' . preg_quote($shortcode, '/') . '(\s[^\]]+)?\].*?\[\/' . preg_quote($shortcode, '/') . '\]/is', '', $raw_content);
            }

            // Render via helper so Fusion Builder / Avada emit the full post body
            // in AJAX context, not just the teaser above <!--more-->.
            $rendered_content = ttswp_render_content_full($raw_content, $post_id);

            $ttswp_regen_rendered_len = strlen( $rendered_content );
            $ttswp_regen_raw_len      = strlen( $post->post_content );
            ttswp_send_remote_debug_event(
                'content_extracted',
                sprintf( 'Regenerate rendered length: %d (raw %d)', $ttswp_regen_rendered_len, $ttswp_regen_raw_len ),
                array(
                    'raw_post_len' => $ttswp_regen_raw_len,
                    'rendered_len' => $ttswp_regen_rendered_len,
                ),
                $ttswp_regen_rendered_len < 500 && $ttswp_regen_raw_len > 1500 ? 'warning' : 'debug',
                $post_id
            );

            // Strip audio/video player HTML elements from rendered content
            $rendered_content = preg_replace('/<(audio|video)[^>]*>.*?<\/\1>/is', '', $rendered_content);
            $rendered_content = preg_replace('/<div[^>]*elementor-widget-tp-audio-player[^>]*>.*?<\/div>\s*<\/div>/is', '', $rendered_content);
            $rendered_content = preg_replace('/<div[^>]*data-widget_type="tp-audio-player[^"]*"[^>]*>.*?<\/div>\s*<\/div>/is', '', $rendered_content);
            $rendered_content = preg_replace('/<div[^>]*tp-audio-player-wrapper[^>]*>.*?<\/div>/is', '', $rendered_content);
            $rendered_content = preg_replace('/<div[^>]*class="[^"]*tp-player[^"]*"[^>]*>.*?<\/div>/is', '', $rendered_content);
            $audio_ui_classes = array('audio-player', 'video-player', 'media-player', 'tp-audio', 'plus-audio', 'theplus-audio', 'mejs-container', 'plyr', 'wp-audio-shortcode', 'wp-video-shortcode', 'elementor-widget-audio', 'elementor-widget-video', 'elementor-widget-tp-audio-player');
            foreach ($audio_ui_classes as $class_pattern) {
                $rendered_content = preg_replace('/<div[^>]*class="[^"]*' . preg_quote($class_pattern, '/') . '[^"]*"[^>]*>.*?<\/div>/is', '', $rendered_content);
            }

            // Use processor to extract content properly
            if (method_exists($processor, 'get_content_from_html')) {
                $fresh_content = $processor->get_content_from_html($rendered_content);
                if (!empty($fresh_content)) {
                    $content_to_use = $fresh_content;
                }
            }
        }
        
        // Generate audio
        $result = $processor->generate_audio($content_to_use, $post_id, $voice_id);

        if ( $result === false ) {
            ttswp_send_remote_debug_event(
                'regenerate_error',
                'Regenerate returned false',
                array( 'content_len' => strlen( (string) $content_to_use ) ),
                'error',
                $post_id
            );
        } else {
            ttswp_send_remote_debug_event(
                'regenerate_success',
                sprintf( 'Regenerate OK — %d chars', strlen( (string) $content_to_use ) ),
                array( 'content_len' => strlen( (string) $content_to_use ) ),
                'info',
                $post_id
            );
        }

        // Handle the result - generate_audio returns string (URL) or false
        if ($result === false) {
            wp_send_json_error(array(
                'message' => __('Failed to regenerate audio. Please check your settings and try again.', 'text-to-speech-tts')
            ));
            return;
        }

        // Success - $result contains the file URL
        wp_send_json_success(array(
            'success' => true,
            'file_url' => $result,
            'message' => __('Audio regenerated successfully.', 'text-to-speech-tts')
        ));
    }

    /**
     * Delete audio via AJAX
     */
    public function delete_audio() {
        // Debug entry - unconditional logging
        error_log('TTS AJAX: delete_audio called - timestamp: ' . current_time('mysql'));

        // Check nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
                error_log('TTS AJAX: Nonce validation failed');
            }
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('edit_posts')) {
            if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
                error_log('TTS AJAX: User capability check failed');
            }
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Get and validate parameters with proper sanitization
        $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
        $element_uid = isset($_POST['element_uid']) ? sanitize_key(wp_unslash($_POST['element_uid'])) : '';

        // Validate post ID
        if (empty($post_id) || !get_post($post_id)) {
            if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
                error_log('TTS AJAX: Invalid post_id=' . $post_id);
            }
            wp_send_json_error(array(
                'message' => __('Invalid post ID provided.', 'text-to-speech-tts')
            ));
            return;
        }

        // If element_uid not provided, use default format
        if (empty($element_uid)) {
            $element_uid = 'post_content_' . $post_id;
            if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
                error_log('TTS AJAX: element_uid not provided, using default: ' . $element_uid);
            }
        }

        if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
            error_log('TTS AJAX: post_id=' . $post_id . ', element_uid=' . $element_uid);
        }

        // Get speech builder instance
        $speech_builder = Mementor_TTS_Speech_Builder::get_instance();

        if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
            error_log('TTS AJAX: Calling delete_element method');
        }

        // Delete element
        $result = $speech_builder->delete_element($post_id, $element_uid);

        if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
            error_log('TTS AJAX: delete_element returned: ' . var_export($result, true));
        }

        if (!$result) {
            if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
                error_log('TTS AJAX: Deletion failed, sending error response');
            }
            wp_send_json_error(array(
                'message' => __('Failed to delete audio. The file may not exist or could not be accessed.', 'text-to-speech-tts')
            ));
            return;
        }

        if (defined('MEMENTOR_TTS_DEBUG') && MEMENTOR_TTS_DEBUG) {
            error_log('TTS AJAX: Deletion successful, sending success response');
        }

        wp_send_json_success(array(
            'message' => __('Audio deleted successfully.', 'text-to-speech-tts')
        ));
    }

    /**
     * Save settings via AJAX
     */
    public function save_settings() {
        // Check nonce
        if (!check_ajax_referer('mementor_tts_nonce', 'nonce', false)) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Array to track processed settings
        $processed_settings = array();

        // Use filter_input to safely retrieve the settings array
        $raw_settings = filter_input(INPUT_POST, 'settings', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);

        // Check if settings exist and process them securely
        if ($raw_settings !== null && $raw_settings !== false) {
            // Unslash the raw settings array (retrieved via filter_input)
            $unslashed_settings = wp_unslash($raw_settings);
            
            // Process and sanitize each setting in a single loop
            foreach ($unslashed_settings as $original_key => $original_value) {
                // Sanitize the key
                $key = sanitize_key($original_key);

                if (empty($key)) {
                    continue;
                }
                
                // Assign the original (unslashed) value for sanitization
                $value_to_sanitize = $original_value;
                
                // Sanitize value based on the SANITIZED key
                switch ($key) {
                    case 'api_key':
                        // API keys should be kept exactly as provided, just trim whitespace
                        $value = trim(sanitize_text_field($value_to_sanitize));
                        // If API key is empty, delete the option instead of saving empty value
                        if (empty($value)) {
                            delete_option('mementor_tts_' . $key);
                            continue 2; // Skip to next setting
                        }
                        break;
                    case 'voice':
                    case 'voice_name':
                    case 'model':
                    case 'language':
                    case 'quality':
                    case 'service':
                        $value = sanitize_text_field($value_to_sanitize);
                        break;
                    case 'stability_preset':
                    case 'similarity_preset':
                    case 'style_preset':
                        $value = absint($value_to_sanitize);
                        break;
                    case 'speaker_boost':
                    case 'cache_enabled':
                    case 'preload_audio':
                    case 'debug_mode':
                    case 'auto_generate':
                    case 'enable_statistics':
                        $value = function_exists('mementor_tts_sanitize_boolean') 
                            ? mementor_tts_sanitize_boolean($value_to_sanitize) 
                            : (bool) $value_to_sanitize;
                        break;
                    // PRO features that need special handling
                    case 'media_library':
                    case 'filename_pattern':
                    case 'storage_location':
                        // Check if user has a paid plan before allowing these settings
                        $is_pro_active = ttswp_has_paid_plan();

                        if ($is_pro_active) {
                            // Only process if PRO is active
                            if ($key === 'filename_pattern') {
                                $value = sanitize_text_field($value_to_sanitize);
                            } elseif ($key === 'media_library') {
                                $value = function_exists('mementor_tts_sanitize_boolean')
                                    ? mementor_tts_sanitize_boolean($value_to_sanitize)
                                    : (bool) $value_to_sanitize;
                            } elseif ($key === 'storage_location') {
                                $valid_locations = array('local', 'dropbox', 's3', 'gcloud');
                                $value = in_array($value_to_sanitize, $valid_locations) 
                                    ? $value_to_sanitize 
                                    : 'local'; // Default to local if invalid
                            }
                        } else {
                            // If PRO is not active, enforce defaults for PRO features
                            if ($key === 'filename_pattern') {
                                $value = 'mementor-[id]-[language]'; // Default pattern
                            } elseif ($key === 'media_library') {
                                $value = 'no'; // Default to no integration
                            } elseif ($key === 'storage_location') {
                                $value = 'local'; // Default to local storage
                            }
                            
                            if (mementor_tts_is_debug_enabled()) {
                                error_log('TTS Plugin: Attempted to save PRO setting ' . $key . ' but PRO is not active. Using default.');
                            }
                        }
                        break;
                    default:
                        // Default sanitization for unknown settings
                        $value = is_array($value_to_sanitize)
                            ? array_map('sanitize_text_field', $value_to_sanitize) // Sanitize each element
                            : sanitize_text_field($value_to_sanitize);
                }

                // Save setting using WordPress options API with SANITIZED key and value
                update_option('mementor_tts_' . $key, $value);

                // Special handling for filename_pattern to ensure it gets saved
                if ($key === 'filename_pattern') {
                    // Update it again with direct value to ensure it's saved
                    update_option('mementor_tts_filename_pattern', sanitize_text_field($value_to_sanitize));
                    if (mementor_tts_is_debug_enabled()) {
                        error_log('TTS Plugin: Explicitly saved filename_pattern as: ' . sanitize_text_field($value_to_sanitize));
                    }
                }
                
                // Track processed settings with SANITIZED key and value
                $processed_settings[$key] = $value;
            }
        }

        // Reset the API instance if API key was changed
        if (isset($processed_settings['api_key']) || isset($unslashed_settings['api_key'])) {
            if (class_exists('Mementor_TTS_ElevenLabs_API')) {
                Mementor_TTS_ElevenLabs_API::reset_instance();
            }
        }

        // Clear dynamic CSS cache if any player-related settings were changed
        // This ensures frontend player immediately reflects color and style changes
        $player_related_keys = array(
            'player_label_margin', 'player_label_size', 'player_label_weight', 'player_label_color',
            'player_bg_color', 'player_border_color', 'player_text_color', 'player_accent_color',
            'play_button_bg', 'timer_color', 'progress_bar_default', 'progress_bar_playing',
            'play_icon_size', 'font_size', 'player_padding', 'player_max_width', 'player_max_width_unit'
        );

        $should_clear_css_cache = false;
        foreach ($player_related_keys as $key) {
            if (isset($processed_settings[$key])) {
                $should_clear_css_cache = true;
                break;
            }
        }

        if ($should_clear_css_cache) {
            delete_transient('mementor_tts_dynamic_css');

            // Also trigger WordPress cache clearing hooks for popular caching plugins
            do_action('litespeed_purge_all');
            do_action('w3tc_flush_all');
            do_action('wp_cache_clear_cache');

            if (mementor_tts_is_debug_enabled()) {
                error_log('TTS: Dynamic CSS cache cleared after player settings update');
            }
        }

        wp_send_json_success(array(
            'message' => __('Settings saved successfully.', 'text-to-speech-tts'),
            'updated_count' => count($processed_settings),
            'cache_cleared' => $should_clear_css_cache
        ));
    }

    /**
     * Test API connection via AJAX
     */
    public function test_api() {
        // Check nonce
        if (!check_ajax_referer('mementor_tts_nonce', 'nonce', false)) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
        }

        // Get API instance
        $api = Mementor_TTS_ElevenLabs_API::get_instance();

        // Test API connection
        $result = $api->validate_api_key();

        if (is_wp_error($result)) {
            wp_send_json_error(array(
                'success' => false,
                'message' => $result->get_error_message()
            ));
            return;
        }

        // Store permission status based on API response
        if (isset($result['data']['missing_user_read']) && $result['data']['missing_user_read']) {
            // Double-check by trying to get user info directly
            $user_info = $api->get_user_info();
            if (!is_wp_error($user_info) && !empty($user_info['data'])) {
                // If user info call succeeds, the API has full permissions
                update_option('mementor_tts_api_key_permissions', 'full');
            } else {
                update_option('mementor_tts_api_key_permissions', 'limited');
            }
        } else if (isset($result['data']['character_count']) || isset($result['data']['character_limit'])) {
            // If we got character count/limit data, it means user_read permission is working
            update_option('mementor_tts_api_key_permissions', 'full');
        } else {
            // Default to full if no specific permission issues detected
            update_option('mementor_tts_api_key_permissions', 'full');
        }

        wp_send_json_success($result);
    }

    /**
     * Get available voices via AJAX
     */
    public function get_voices() {
        // Check nonce first for security
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Get parameters with proper sanitization
        $language = isset($_POST['language']) ? sanitize_text_field(wp_unslash($_POST['language'])) : '';
        $model_id = isset($_POST['model_id']) ? sanitize_text_field(wp_unslash($_POST['model_id'])) : '';
        
        // Fetch voices via SaaS API (proxied from ElevenLabs)
        $api_client = Mementor_TTS_Api_Client::get_instance();
        $result = $api_client->get_voices();

        if (is_wp_error($result)) {
            $response = array(
                'success' => false,
                'data' => array(
                    'message' => $result->get_error_message(),
                    'voices' => array()
                )
            );
        } else {
            $response = array(
                'success' => true,
                'data' => array(
                    'message' => __('Voices loaded successfully.', 'text-to-speech-tts'),
                    'voices' => $result['voices']
                )
            );
        }
        
        // Send response
        wp_send_json($response);
    }

    /**
     * Clear voice cache
     */
    public function clear_voice_cache() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error('Invalid nonce');
            return;
        }
        
        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Clear the SaaS voices cache
        delete_transient( 'tts_saas_voices' );

        wp_send_json_success();
    }

    /**
     * Save voice via AJAX
     */
    public function save_voice() {
        // Check nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
        }

        // Check if user has a paid plan
        $is_pro_active = ttswp_has_paid_plan();

        // Get and validate parameters with proper sanitization
        $voice_id = isset($_POST['voice_id']) ? sanitize_text_field(wp_unslash($_POST['voice_id'])) : '';
        $voice_name = isset($_POST['voice_name']) ? sanitize_text_field(wp_unslash($_POST['voice_name'])) : '';
        $language = isset($_POST['language']) ? sanitize_text_field(wp_unslash($_POST['language'])) : '';
        $free_users_allowed = isset($_POST['free_users_allowed']) ? filter_var($_POST['free_users_allowed'], FILTER_VALIDATE_BOOLEAN) : false;
        
        // Check if this is a PRO voice based on free_users_allowed flag
        $is_pro_voice = false;
        if (isset($_POST['free_users_allowed'])) {
            // If we have the free_users_allowed flag from the frontend, use it directly
            $is_pro_voice = !$free_users_allowed;
        }
        
        // Get other voice-related settings
        $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
        $quality = isset($_POST['quality']) ? sanitize_text_field(wp_unslash($_POST['quality'])) : '';
        $similarity = isset($_POST['similarity_preset']) ? absint(wp_unslash($_POST['similarity_preset'])) : 75;
        $style = isset($_POST['style_preset']) ? absint(wp_unslash($_POST['style_preset'])) : 0;
        $privacy_mode = isset($_POST['privacy_mode']) ? (bool)wp_unslash($_POST['privacy_mode']) : false;

        // Enforce restrictions for PRO features if PRO is not active
        if (!$is_pro_active) {
            // CRITICAL: Prevent selection of PRO voices for free users
            if ($is_pro_voice) {
                // Get the current voice settings to use as fallback
                $current_voice_id = get_option('mementor_tts_voice', '');
                $current_voice_name = get_option('mementor_tts_voice_name', '');
                
                // If the current voice is also a PRO voice or not set, use a default free voice
                $default_free_voice_id = 'EXAVITQu4vr4xnSDxMaL';  // Rachel voice ID (free English voice)
                $default_free_voice_name = 'Rachel';  // Rachel voice name
                
                // Override the selected PRO voice with a free voice
                $voice_id = !empty($current_voice_id) ? $current_voice_id : $default_free_voice_id;
                $voice_name = !empty($current_voice_name) ? $current_voice_name : $default_free_voice_name;

                if (mementor_tts_is_debug_enabled()) {
                    error_log('TTS Plugin: Attempted to save PRO voice but PRO is not active. Using free voice instead.');
                }

                // Send a specific error message to the client
                wp_send_json_error(array(
                    'message' => __('PRO voices are only available with the PRO version. Please upgrade or select a free voice.', 'text-to-speech-tts'),
                    'voice_id' => $voice_id,
                    'voice_name' => $voice_name
                ));
                return;
            }

            // Restrict model to free model
            if ($model !== 'eleven_flash_v2_5') {
                $model = 'eleven_flash_v2_5'; // Default to Flash model
                if (mementor_tts_is_debug_enabled()) {
                    error_log('TTS Plugin: Attempted to save PRO model but PRO is not active. Defaulting to eleven_flash_v2_5.');
                }
            }

            // Restrict quality to free quality
            if ($quality !== '128') {
                $quality = '128'; // Default to 128kbps
                if (mementor_tts_is_debug_enabled()) {
                    error_log('TTS Plugin: Attempted to save PRO quality but PRO is not active. Defaulting to 128kbps.');
                }
            }

            // Disable privacy mode
            if ($privacy_mode) {
                $privacy_mode = false;
                if (mementor_tts_is_debug_enabled()) {
                    error_log('TTS Plugin: Attempted to enable privacy mode but PRO is not active. Disabling.');
                }
            }
            
            // Reset similarity and style to defaults
            $similarity = 75;
            $style = 0;
        }
        
        // Save voice ID and name settings
        update_option('mementor_tts_voice', $voice_id);
        update_option('mementor_tts_voice_name', $voice_name);
        
        // Save language if provided
        if (!empty($language)) {
            update_option('mementor_tts_language', $language);
            
            // For non-English languages, ensure we're using a model that supports language enforcement
            if ($language !== 'en') {
                $current_model = get_option('mementor_tts_model', 'eleven_flash_v2_5');
                
                // If using monolingual model with non-English language, switch to Flash model
                if ($current_model === 'eleven_monolingual_v1') {
                    update_option('mementor_tts_model', 'eleven_flash_v2_5');
                }
            }
        }
        
        // Save other voice settings only if explicitly provided in the request
        if (!empty($model)) {
            update_option('mementor_tts_model', $model);
        }

        if (!empty($quality)) {
            update_option('mementor_tts_quality', $quality);
        }

        // Only save these settings if explicitly sent in the request
        if (isset($_POST['similarity_preset'])) {
            update_option('mementor_tts_similarity_preset', $similarity);
        }
        if (isset($_POST['style_preset'])) {
            update_option('mementor_tts_style_preset', $style);
        }
        if (isset($_POST['privacy_mode'])) {
            update_option('mementor_tts_privacy_mode', $privacy_mode ? '1' : '');
        }

        wp_send_json_success(array(
            'message' => __('Voice settings saved successfully.', 'text-to-speech-tts'),
            'voice_id' => $voice_id,
            'voice_name' => $voice_name,
            'language' => $language
        ));
    }

    /**
     * Generate audio for shortcode
     */
    public function ajax_generate_shortcode_audio() {
        // Start logging
        if (mementor_tts_is_debug_enabled()) {
            error_log('====== STARTING SHORTCODE AUDIO GENERATION AJAX REQUEST ======');
        }

        // Check nonce for security
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            if (mementor_tts_is_debug_enabled()) {
                error_log('TTS Shortcode: Nonce verification failed');
                error_log('Provided nonce: ' . (isset($_POST['nonce']) ? substr(sanitize_key(wp_unslash($_POST['nonce'])), 0, 5) . '...' : 'none'));
            }
            wp_send_json_error([
                'message' => __('Security check failed', 'text-to-speech-tts')
            ]);
            return;
        }

        // Get parameters with proper sanitization
        // Important: For text we need to use wp_unslash to remove any backslashes added by WordPress
        $raw_text = isset($_POST['text']) ? $_POST['text'] : '';
        $text = wp_unslash($raw_text); // First unslash to remove any WordPress-added backslashes
        $text = sanitize_textarea_field($text); // Then sanitize without losing content

        $element_uid = isset($_POST['element_uid']) ? sanitize_key($_POST['element_uid']) : '';
        $voice_id = isset($_POST['voice_id']) ? sanitize_text_field($_POST['voice_id']) : '';

        // Debug logging
        if (mementor_tts_is_debug_enabled()) {
            error_log('TTS Shortcode AJAX - Element UID: ' . $element_uid);
            error_log('TTS Shortcode AJAX - Voice ID: ' . $voice_id);
            error_log('TTS Shortcode AJAX - Raw Text Length: ' . strlen($raw_text));
            if (strlen($raw_text) > 0) {
                error_log('TTS Shortcode AJAX - Raw Text Preview: "' . substr($raw_text, 0, 100) . (strlen($raw_text) > 100 ? '...' : '') . '"');
            } else {
                error_log('TTS Shortcode AJAX - WARNING: Raw text is empty!');
            }

            error_log('TTS Shortcode AJAX - Processed Text Length: ' . strlen($text));
            if (strlen($text) > 0) {
                error_log('TTS Shortcode AJAX - Processed Text Preview: "' . substr($text, 0, 100) . (strlen($text) > 100 ? '...' : '') . '"');
            } else {
                error_log('TTS Shortcode AJAX - WARNING: Processed text is empty!');
            }

            // Check for potential encoding issues
            if (strlen($raw_text) > 0 && strlen($text) === 0) {
                error_log('TTS Shortcode AJAX - ERROR: Text lost during sanitization! This might be an encoding issue.');
            }
        }

        // Validate required fields
        if (empty($text)) {
            if (mementor_tts_is_debug_enabled()) {
                error_log('TTS Shortcode: Empty text provided');
            }
            wp_send_json_error([
                'message' => __('No text provided', 'text-to-speech-tts')
            ]);
            return;
        }

        if (empty($element_uid)) {
            if (mementor_tts_is_debug_enabled()) {
                error_log('TTS Shortcode: No element UID provided');
            }
            wp_send_json_error([
                'message' => __('Missing element identifier', 'text-to-speech-tts')
            ]);
            return;
        }

        // Use the plugin's TTS language setting directly.
        // ICL_LANGUAGE_CODE reflects the admin page language (usually 'en'),
        // not the user's chosen TTS language.
        $language_code = get_option('mementor_tts_language', 'en');

        $upload_dir = wp_upload_dir();
        $file_path = $upload_dir['basedir'] . '/text-to-speech-tts/' . $element_uid . '-' . $language_code . '.mp3';
        $file_url = $upload_dir['baseurl'] . '/text-to-speech-tts/' . $element_uid . '-' . $language_code . '.mp3';

        // If file already exists, return it
        if (file_exists($file_path) && filesize($file_path) > 1000) {
            if (mementor_tts_is_debug_enabled()) {
                error_log('TTS Shortcode: Audio file already exists at ' . $file_url . ' (size: ' . filesize($file_path) . ' bytes)');
                error_log('====== SHORTCODE AUDIO GENERATION AJAX REQUEST COMPLETED - USING EXISTING FILE ======');
            }
            wp_send_json_success([
                'message' => __('Audio already exists', 'text-to-speech-tts'),
                'audio_url' => $file_url
            ]);
            return;
        }

        // Process the text for TTS
        if (mementor_tts_is_debug_enabled()) {
            error_log('TTS Shortcode: Loading processor class...');
        }

        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-processor.php';
        $processor = Mementor_TTS_Processor::get_instance();

        if (mementor_tts_is_debug_enabled()) {
            error_log('TTS Shortcode: Processor instance created, calling generate_shortcode_audio method');
        }

        // Get intro/outro inclusion options from the request
        $include_intro = isset($_POST['include_intro']) ? $_POST['include_intro'] === '1' : true;
        $include_outro = isset($_POST['include_outro']) ? $_POST['include_outro'] === '1' : true;

        // Generate audio - pass the options array with element_uid
        $options = array(
            'element_uid' => $element_uid,
            'language_code' => $language_code,
            'include_intro' => $include_intro,
            'include_outro' => $include_outro
        );

        $result = $processor->generate_shortcode_audio($text, $element_uid, $voice_id, $options);

        if (mementor_tts_is_debug_enabled()) {
            error_log('TTS Shortcode: Processor result - ' . (isset($result['success']) && $result['success'] ? 'Success' : 'Failed'));
            if (isset($result['file_url'])) {
                error_log('TTS Shortcode: Generated file URL - ' . $result['file_url']);
            }
            if (isset($result['message'])) {
                error_log('TTS Shortcode: Result message - ' . $result['message']);
            }
        }

        // Check for success
        if (!$result || !isset($result['success']) || !$result['success']) {
            $error_message = isset($result['message']) ? $result['message'] : __('Unknown error generating audio', 'text-to-speech-tts');
            if (mementor_tts_is_debug_enabled()) {
                error_log('TTS Shortcode: Error generating audio - ' . $error_message);
                error_log('====== SHORTCODE AUDIO GENERATION AJAX REQUEST COMPLETED - ERROR ======');
            }
            wp_send_json_error([
                'message' => $error_message
            ]);
            return;
        }

        // Return success with file URL
        if (mementor_tts_is_debug_enabled()) {
            error_log('TTS Shortcode: Audio generated successfully, returning URL: ' . $result['file_url']);
            error_log('====== SHORTCODE AUDIO GENERATION AJAX REQUEST COMPLETED - SUCCESS ======');
        }
        
        wp_send_json_success([
            'message' => __('Audio generated successfully', 'text-to-speech-tts'),
            'audio_url' => $result['file_url']
        ]);
    }
    
    /**
     * Debug credits system
     */
    public function debug_credits() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array('message' => __('Security check failed.', 'text-to-speech-tts')));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Get all relevant information
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-user-credits.php';
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-elevenlabs-api.php';
        
        $user_credits = Mementor_TTS_User_Credits::get_instance();
        $api = Mementor_TTS_ElevenLabs_API::get_instance();
        
        $debug_info = array(
            'user_id' => get_current_user_id(),
            'credits_remaining' => $user_credits->get_remaining_credits(),
            'has_credits' => $user_credits->has_credits_remaining(),
            'is_reset_attempt' => $user_credits->detect_reset_attempt(),
            'api_key_in_db' => !empty(get_option('mementor_tts_api_key')),
            'site_url' => site_url(),
            'is_playground' => method_exists($user_credits, 'is_wordpress_playground') ? $user_credits->is_wordpress_playground() : 'method not found',
            'credits_data' => $user_credits->get_user_credits()
        );
        
        wp_send_json_success($debug_info);
    }
    
    /**
     * Clear all saved voices
     */
    public function clear_all_saved_voices() {
        // Check nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }
        
        // Since saved voices are stored in localStorage on the client side,
        // we just need to send a success response. The JavaScript will handle
        // clearing the localStorage.
        wp_send_json_success(array(
            'message' => __('All saved voices have been cleared successfully.', 'text-to-speech-tts')
        ));
    }

    /**
     * Save model selection from generator page
     */
    public function save_model() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Security check failed.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Get and sanitize the model
        $model = isset($_POST['model']) ? sanitize_text_field($_POST['model']) : '';

        // Validate model value
        $valid_models = array('eleven_flash_v2_5', 'eleven_turbo_v2_5', 'eleven_multilingual_v2', 'eleven_v3');
        if (!in_array($model, $valid_models)) {
            wp_send_json_error(array(
                'message' => __('Invalid model selection.', 'text-to-speech-tts')
            ));
            return;
        }

        // Save the model
        update_option('mementor_tts_model', $model);

        wp_send_json_success(array(
            'message' => __('Model updated successfully.', 'text-to-speech-tts'),
            'model' => $model
        ));
    }

    /**
     * Transcribe audio/video file to text
     */
    public function transcribe_audio() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_stt_nonce')) {
            wp_send_json_error(array('message' => __('Security check failed.', 'text-to-speech-tts')));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')));
            return;
        }

        // Check for file upload
        if (empty($_FILES['file'])) {
            wp_send_json_error(array('message' => __('No file uploaded.', 'text-to-speech-tts')));
            return;
        }

        $file = $_FILES['file'];

        // Validate file upload
        if ($file['error'] !== UPLOAD_ERR_OK) {
            wp_send_json_error(array('message' => __('File upload error.', 'text-to-speech-tts')));
            return;
        }

        // Reject files that are too small to be valid audio
        if ($file['size'] < 1000) {
            wp_send_json_error(array('message' => __('File appears to be corrupt or too small. Please upload a valid audio file.', 'text-to-speech-tts')));
            return;
        }

        // Get options
        $language = isset($_POST['language']) ? sanitize_text_field($_POST['language']) : 'auto';
        $tag_events = isset($_POST['tag_events']) && $_POST['tag_events'] === '1';
        $include_timestamps = isset($_POST['include_timestamps']) && $_POST['include_timestamps'] === '1';

        $token = Mementor_TTS_Connect::get_site_token();
        if (empty($token)) {
            wp_send_json_error(array('message' => __('Site is not connected to TTSWP. Please connect your site first.', 'text-to-speech-tts')));
            return;
        }

        // Save to a publicly accessible temp location so ElevenLabs can fetch it
        $upload_dir = wp_upload_dir();
        $tts_dir = $upload_dir['basedir'] . '/text-to-speech-tts';
        $safe_name = sanitize_file_name($file['name']);
        $filetype = wp_check_filetype($safe_name, array(
            'mp3'  => 'audio/mpeg',
            'mp4'  => 'audio/mp4',
            'm4a'  => 'audio/x-m4a',
            'wav'  => 'audio/wav',
            'ogg'  => 'audio/ogg',
            'flac' => 'audio/flac',
        ));
        if (empty($filetype['ext'])) {
            wp_send_json_error(array('message' => __('Invalid file type. Allowed: mp3, mp4, m4a, wav, ogg, flac.', 'text-to-speech-tts')));
            return;
        }

        $temp_filename = 'stt_' . uniqid() . '_' . $safe_name;
        $temp_path = $tts_dir . '/' . $temp_filename;

        if (!file_exists($file['tmp_name'])) {
            wp_send_json_error(array('message' => __('Uploaded file not found. It may have expired.', 'text-to-speech-tts')));
            return;
        }

        $copied = copy($file['tmp_name'], $temp_path);
        if (!$copied) {
            wp_send_json_error(array('message' => __('Failed to save uploaded file.', 'text-to-speech-tts')));
            return;
        }

        $source_url = $upload_dir['baseurl'] . '/text-to-speech-tts/' . $temp_filename;
        $api_url = Mementor_TTS_Connect::get_app_url() . '/api/v1/transcribe';
        $response = wp_remote_post($api_url, array(
            'timeout' => 300,
            'headers' => array(
                'X-Site-Token' => $token,
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ),
            'body' => wp_json_encode(array(
                'source_url' => $source_url,
                'language' => $language,
                'tag_events' => $tag_events,
            )),
        ));

        // Clean up temp file immediately
        @unlink($temp_path);

        if (is_wp_error($response)) {
            wp_send_json_error(array('message' => $response->get_error_message()));
            return;
        }

        $response_code = wp_remote_retrieve_response_code($response);
        $response_body = json_decode(wp_remote_retrieve_body($response), true);

        if ($response_code !== 200) {
            $error_message = $response_body['error'] ?? __('Transcription failed', 'text-to-speech-tts');
            wp_send_json_error(array('message' => $error_message));
            return;
        }

        // Save to database
        require_once MEMENTOR_TTS_PLUGIN_DIR . 'includes/class-mementor-tts-transcriptions.php';
        $transcriptions = Mementor_TTS_Transcriptions::get_instance();

        $transcription_id = $transcriptions->save_transcription(array(
            'filename' => sanitize_file_name($file['name']),
            'original_filename' => sanitize_text_field($file['name']),
            'file_size' => $file['size'],
            'file_type' => sanitize_text_field($file['type']),
            'language' => $language,
            'transcription_text' => $response_body['text'] ?? '',
            'transcription_data' => $response_body['data'] ?? array(),
            'tag_events' => $tag_events,
            'include_timestamps' => $include_timestamps,
        ));

        wp_send_json_success(array(
            'text' => $response_body['text'] ?? '',
            'data' => $response_body['data'] ?? array(),
            'id' => $transcription_id,
        ));
    }

    /**
     * Delete a transcription
     */
    public function delete_transcription() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['nonce'])), 'mementor_stt_nonce')) {
            wp_send_json_error(array('message' => __('Security check failed.', 'text-to-speech-tts')));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')));
            return;
        }

        // Get transcription ID
        $id = isset($_POST['id']) ? absint($_POST['id']) : 0;

        if (!$id) {
            wp_send_json_error(array('message' => __('Invalid transcription ID.', 'text-to-speech-tts')));
            return;
        }

        // Delete transcription
        require_once MEMENTOR_TTS_PLUGIN_DIR . 'includes/class-mementor-tts-transcriptions.php';
        $transcriptions = Mementor_TTS_Transcriptions::get_instance();

        $deleted = $transcriptions->delete_transcription($id);

        if ($deleted) {
            wp_send_json_success(array('message' => __('Transcription deleted successfully.', 'text-to-speech-tts')));
        } else {
            wp_send_json_error(array('message' => __('Failed to delete transcription.', 'text-to-speech-tts')));
        }
    }

    /**
     * Refresh ElevenLabs statistics by clearing cached data
     */
    public function refresh_stats() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Clear ElevenLabs API statistics transients
        delete_transient('mementor_tts_user_info');
        delete_transient('mementor_tts_voices');

        // Clear any voice cache transients (pattern: mementor_tts_elevenlabs_voices_*)
        global $wpdb;
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->options}
            WHERE option_name LIKE %s
            OR option_name LIKE %s",
            '_transient_mementor_tts_elevenlabs_voices_%',
            '_transient_timeout_mementor_tts_elevenlabs_voices_%'
        ));

        // Log the refresh action if debug mode is enabled
        if (mementor_tts_is_debug_enabled()) {
            error_log('TTS: ElevenLabs statistics cache cleared by user ' . get_current_user_id());
        }

        wp_send_json_success(array(
            'message' => __('Statistics refreshed successfully.', 'text-to-speech-tts')
        ));
    }

    /**
     * Test S3 connection via AJAX
     */
    public function test_s3_connection() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'mementor_tts_nonce')) {
            wp_send_json_error(array(
                'message' => __('Invalid security token.', 'text-to-speech-tts')
            ));
            return;
        }

        // Check user capabilities
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'text-to-speech-tts')
            ));
            return;
        }

        // Get S3 settings from POST data
        if (!isset($_POST['settings']) || !is_array($_POST['settings'])) {
            wp_send_json_error(array(
                'message' => __('Invalid settings data.', 'text-to-speech-tts')
            ));
            return;
        }

        $settings = array_map('sanitize_text_field', wp_unslash($_POST['settings']));

        // Handle secret key - if empty, use existing key from database
        if (empty($settings['secret_key'])) {
            $stored_secret_key = get_option('mementor_tts_s3_secret_key', '');
            if (!empty($stored_secret_key)) {
                // Try to decrypt the stored key
                require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-encryption.php';
                $encryption = new Mementor_TTS_Encryption();
                $decrypted_key = $encryption->decrypt($stored_secret_key);

                // If decryption succeeds, use decrypted key
                // If decryption fails, assume it's an old unencrypted key and use as-is
                if ($decrypted_key !== false) {
                    $settings['secret_key'] = $decrypted_key;
                } else {
                    // Backward compatibility for unencrypted keys
                    $settings['secret_key'] = $stored_secret_key;
                }
            }
        }

        // Validate required fields
        if (empty($settings['access_key']) || empty($settings['secret_key']) ||
            empty($settings['region']) || empty($settings['bucket'])) {
            wp_send_json_error(array(
                'message' => __('Please fill in all required fields (Access Key, Secret Key, Region, and Bucket).', 'text-to-speech-tts')
            ));
            return;
        }

        // Include S3 handler class
        require_once plugin_dir_path(__FILE__) . 'class-mementor-tts-s3-handler.php';

        // Create S3 handler with provided settings
        $s3_handler = new Mementor_TTS_S3_Handler(array(
            'access_key' => $settings['access_key'],
            'secret_key' => $settings['secret_key'],
            'region' => $settings['region'],
            'bucket' => $settings['bucket'],
            'path' => isset($settings['path']) ? $settings['path'] : 'tts-audio'
        ));

        // Test connection
        $result = $s3_handler->test_connection();

        if ($result['success']) {
            wp_send_json_success(array(
                'message' => $result['message']
            ));
        } else {
            wp_send_json_error(array(
                'message' => $result['message']
            ));
        }
    }

    /**
     * Recursively extract text content from Elementor data structure.
     *
     * When Elementor's frontend rendering fails in AJAX context, we fall back
     * to parsing the raw _elementor_data JSON to get the actual text content.
     *
     * @since 1.9.4
     * @param array $data Decoded Elementor data array.
     * @return string HTML paragraphs extracted from the data.
     */
    private function extract_text_from_elementor_data($data) {
        $html = '';

        foreach ($data as $element) {
            if (!is_array($element)) {
                continue;
            }

            // Check for CSS classes that should be excluded (e.g., no-audio)
            $css_classes = '';
            if (isset($element['settings']['css_classes'])) {
                $css_classes = $element['settings']['css_classes'];
            }
            if (isset($element['settings']['_css_classes'])) {
                $css_classes .= ' ' . $element['settings']['_css_classes'];
            }

            // Skip elements with no-audio class
            if (strpos($css_classes, 'no-audio') !== false) {
                if (mementor_tts_is_debug_enabled()) {
                    error_log('[TTS AJAX] Skipping Elementor element with no-audio class');
                }
                continue;
            }

            // Extract text from known content fields in settings
            if (isset($element['settings']) && is_array($element['settings'])) {
                $text_fields = array('editor', 'title', 'description', 'text', 'content');
                foreach ($text_fields as $field) {
                    if (!empty($element['settings'][$field]) && is_string($element['settings'][$field])) {
                        $field_html = $element['settings'][$field];
                        // Only include if it contains actual text (not just HTML tags)
                        $stripped = trim(wp_strip_all_tags($field_html));
                        if (!empty($stripped)) {
                            $html .= $field_html;
                        }
                    }
                }
            }

            // Recurse into nested elements
            if (!empty($element['elements']) && is_array($element['elements'])) {
                $html .= $this->extract_text_from_elementor_data($element['elements']);
            }
        }

        return $html;
    }

    /**
     * Format a numeric price for TTS readability.
     * Outputs "95 dollars" instead of "$95.00" so TTS engines read it correctly.
     *
     * @param string|float $price The numeric price value.
     * @return string TTS-friendly price string.
     */
    private function format_price_for_speech($price) {
        $price = floatval($price);
        $currency = get_woocommerce_currency();

        $currency_names = array(
            'USD' => __('dollars', 'text-to-speech-tts'),
            'EUR' => __('euros', 'text-to-speech-tts'),
            'GBP' => __('pounds', 'text-to-speech-tts'),
            'NOK' => __('kroner', 'text-to-speech-tts'),
            'SEK' => __('kronor', 'text-to-speech-tts'),
            'DKK' => __('kroner', 'text-to-speech-tts'),
            'CAD' => __('Canadian dollars', 'text-to-speech-tts'),
            'AUD' => __('Australian dollars', 'text-to-speech-tts'),
            'JPY' => __('yen', 'text-to-speech-tts'),
            'CHF' => __('francs', 'text-to-speech-tts'),
            'INR' => __('rupees', 'text-to-speech-tts'),
            'BRL' => __('reais', 'text-to-speech-tts'),
            'MXN' => __('pesos', 'text-to-speech-tts'),
            'PLN' => __('zloty', 'text-to-speech-tts'),
            'CNY' => __('yuan', 'text-to-speech-tts'),
            'KRW' => __('won', 'text-to-speech-tts'),
        );

        $currency_name = isset($currency_names[$currency]) ? $currency_names[$currency] : $currency;

        // Drop cents if .00
        if (floor($price) == $price) {
            return number_format($price, 0) . ' ' . $currency_name;
        }

        return number_format($price, 2) . ' ' . $currency_name;
    }

    /**
     * Assemble text content for a WooCommerce product based on product audio settings.
     *
     * @param int $post_id The product post ID.
     * @return string Assembled text with parts joined by ' -- '.
     */
    public function assemble_product_text($post_id) {
        if (!function_exists('wc_get_product')) {
            return '';
        }

        $product = wc_get_product($post_id);
        if (!$product) {
            return '';
        }

        $parts = array();

        // 1. Title
        if (get_option('mementor_tts_product_include_title', 1)) {
            $name = $product->get_name();
            if (!empty($name)) {
                $parts[] = $name;
            }
        }

        // 2. Price
        if (get_option('mementor_tts_product_include_price', 1)) {
            $regular_price = $product->get_regular_price();
            $sale_price = $product->get_sale_price();

            if (!empty($sale_price) && $sale_price !== $regular_price) {
                $tpl = get_option('mementor_tts_product_text_sale', '');
                if (empty($tpl)) {
                    /* translators: 1: Original product price, spoken (e.g. "ninety-five dollars"). 2: Discounted sale price, spoken. */
                    $tpl = __('Was %1$s, now %2$s', 'text-to-speech-tts');
                }
                $parts[] = sprintf(
                    $tpl,
                    $this->format_price_for_speech($regular_price),
                    $this->format_price_for_speech($sale_price)
                );
            } elseif (!empty($regular_price)) {
                $tpl = get_option('mementor_tts_product_text_price', '');
                if (empty($tpl)) {
                    /* translators: %s: Formatted product price, e.g. "95 dollars". */
                    $tpl = __('Price: %s', 'text-to-speech-tts');
                }
                $parts[] = sprintf($tpl, $this->format_price_for_speech($regular_price));
            }
        }

        // 3. Stock status
        if (get_option('mementor_tts_product_include_stock', 1)) {
            if ($product->is_in_stock()) {
                $stock_qty = $product->get_stock_quantity();
                if ($stock_qty !== null && $stock_qty > 0) {
                    $tpl = get_option('mementor_tts_product_text_stock_qty', '');
                    if (empty($tpl)) {
                        /* translators: %d: Number of items in stock. */
                        $tpl = __('%d in stock', 'text-to-speech-tts');
                    }
                    $parts[] = sprintf($tpl, $stock_qty);
                } else {
                    $text = get_option('mementor_tts_product_text_in_stock', '');
                    $parts[] = !empty($text) ? $text : __('In stock', 'text-to-speech-tts');
                }
            } else {
                $text = get_option('mementor_tts_product_text_out_of_stock', '');
                $parts[] = !empty($text) ? $text : __('Out of stock', 'text-to-speech-tts');
            }
        }

        // 4. Categories
        if (get_option('mementor_tts_product_include_category', 1)) {
            $terms = wp_get_post_terms($post_id, 'product_cat', array('fields' => 'names'));
            if (!is_wp_error($terms) && !empty($terms)) {
                if (count($terms) === 1) {
                    $tpl = get_option('mementor_tts_product_text_category', '');
                    if (empty($tpl)) {
                        /* translators: %s: Single product category name. */
                        $tpl = __('Category: %s', 'text-to-speech-tts');
                    }
                    $parts[] = sprintf($tpl, $terms[0]);
                } else {
                    $tpl = get_option('mementor_tts_product_text_categories', '');
                    if (empty($tpl)) {
                        /* translators: %s: Comma-separated list of product category names. */
                        $tpl = __('Categories: %s', 'text-to-speech-tts');
                    }
                    $parts[] = sprintf($tpl, implode(', ', $terms));
                }
            }
        }

        // 5. Product image text
        if (get_option('mementor_tts_product_include_image', 0)) {
            $thumbnail_id = $product->get_image_id();
            if ($thumbnail_id) {
                $image_parts = array();
                $attachment = get_post($thumbnail_id);

                if (get_option('mementor_tts_product_image_alt', 1)) {
                    $alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
                    if (!empty($alt)) {
                        $image_parts[] = $alt;
                    }
                }
                if (get_option('mementor_tts_product_image_title', 1)) {
                    if ($attachment && !empty($attachment->post_title)) {
                        $image_parts[] = $attachment->post_title;
                    }
                }
                if (get_option('mementor_tts_product_image_caption', 0)) {
                    if ($attachment && !empty($attachment->post_excerpt)) {
                        $image_parts[] = $attachment->post_excerpt;
                    }
                }
                if (get_option('mementor_tts_product_image_description', 0)) {
                    if ($attachment && !empty($attachment->post_content)) {
                        $image_parts[] = $attachment->post_content;
                    }
                }

                if (!empty($image_parts)) {
                    $parts[] = implode('. ', array_unique($image_parts));
                }
            }
        }

        // 6. Short description
        if (get_option('mementor_tts_product_include_short_desc', 1)) {
            $short_desc = $product->get_short_description();
            if (!empty($short_desc)) {
                $parts[] = wp_strip_all_tags(strip_shortcodes($short_desc));
            }
        }

        // 7. Long description
        if (get_option('mementor_tts_product_include_long_desc', 1)) {
            $long_desc = $product->get_description();
            if (!empty($long_desc)) {
                $parts[] = wp_strip_all_tags(strip_shortcodes($long_desc));
            }
        }

        // Clean up each part
        $cleaned_parts = array();
        foreach ($parts as $part) {
            $part = str_replace('&nbsp;', ' ', $part);
            $part = html_entity_decode($part, ENT_QUOTES, 'UTF-8');
            $part = preg_replace('/\s+/', ' ', $part);
            $part = trim($part);
            if (!empty($part)) {
                $cleaned_parts[] = $part;
            }
        }

        return implode(' -- ', $cleaned_parts);
    }
}