<?php
/**
 * TalkGenAI Admin Class
 * Handles WordPress admin interface
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit('Direct access not allowed.');
}

class TalkGenAI_Admin {
    
    /**
     * Plugin components
     */
    private $database;
    private $api;
    private $security;
    private $file_generator;
    
    /**
     * Constructor
     */
    public function __construct($database, $api, $security) {
        $this->database = $database;
        $this->api = $api;
        $this->security = $security;
        
        // Initialize file generator
        if (!class_exists('TalkGenAI_File_Generator')) {
            require_once plugin_dir_path(__FILE__) . 'class-talkgenai-file-generator.php';
        }
        $this->file_generator = new TalkGenAI_File_Generator();
        
        // Add admin hooks
        add_action('admin_init', array($this, 'admin_init'));
        add_action('admin_notices', array($this, 'admin_notices'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
        
        // Hide non-critical admin notices on TalkGenAI pages for clean interface
        // Uses selective CSS hiding - keeps critical errors/warnings visible
        add_action('admin_head', array($this, 'hide_unimportant_notices_on_talkgenai_pages'));
    }
    
    /**
     * Sanitize decoded JSON data recursively (WordPress.org requirement)
     * Must be called immediately after json_decode() to satisfy WordPress.org guidelines
     */
    private function sanitize_decoded_json($data) {
        if (!is_array($data)) {
            if (is_string($data)) {
                return sanitize_textarea_field($data);
            }
            return $data; // Numbers, booleans, null
        }
        
        $sanitized = array();
        foreach ($data as $key => $value) {
            // CRITICAL: Preserve camelCase keys for JSON API compatibility
            // sanitize_key() lowercases everything, breaking appClass/appType
            // Instead, sanitize to allow alphanumeric + underscores while preserving case
            $safe_key = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
            
            if (is_array($value)) {
                $sanitized[$safe_key] = $this->sanitize_decoded_json($value);
            } elseif (is_string($value)) {
                // Use sanitize_textarea_field to preserve line breaks in descriptions
                $sanitized[$safe_key] = sanitize_textarea_field($value);
            } else {
                $sanitized[$safe_key] = $value; // Numbers, booleans, null
            }
        }
        
        return $sanitized;
    }
    
    /**
     * Admin initialization
     */
    public function admin_init() {
        // Check requirements
        if (!talkgenai_display_requirements_check()) {
            return;
        }
        
        // Register settings
        $this->register_settings();
    }
    
    /**
     * Enqueue admin scripts and styles using WordPress best practices
     */
    public function enqueue_admin_assets($hook) {
        // Only load on TalkGenAI pages
        if (strpos($hook, 'talkgenai') === false) {
            return;
        }
        
        // Enqueue a dummy script handle for inline script attachment
        // WordPress requires a registered script to attach inline scripts
        wp_register_script('talkgenai-admin-base', false, array('jquery'), TALKGENAI_VERSION, true);
        wp_enqueue_script('talkgenai-admin-base');
        
        // Enqueue a dummy style handle for inline style attachment
        wp_register_style('talkgenai-admin-base', false, array(), TALKGENAI_VERSION);
        wp_enqueue_style('talkgenai-admin-base');
        
        // Add apps list page styles using wp_add_inline_style()
        if ($hook === 'toplevel_page_talkgenai-apps' || strpos($hook, 'talkgenai-apps') !== false) {
            $this->enqueue_apps_list_styles();
            $this->enqueue_apps_list_scripts();
        }
        
        // Add edit page scripts using wp_add_inline_script()
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameters for script enqueuing; capability checks enforced via current_user_can()
        if (isset($_GET['action']) && sanitize_key($_GET['action']) === 'edit' && (isset($_GET['id']) || isset($_GET['app_id']))) {
            $this->enqueue_edit_page_scripts();
        }
    }
    
    /**
     * Enqueue apps list page styles (WordPress best practice: wp_add_inline_style)
     */
    private function enqueue_apps_list_styles() {
        $css = $this->get_apps_list_page_css();
        wp_add_inline_style('talkgenai-admin-base', $css);
    }
    
    /**
     * Enqueue apps list page scripts (WordPress best practice: wp_add_inline_script)
     */
    private function enqueue_apps_list_scripts() {
        $js = $this->get_apps_list_page_js();
        wp_add_inline_script('talkgenai-admin-base', $js);
    }
    
    /**
     * Enqueue edit page scripts (WordPress best practice: wp_add_inline_script)
     */
    private function enqueue_edit_page_scripts() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for script enqueuing; capability checks enforced via current_user_can()
        $app_id = isset($_GET['app_id']) ? intval($_GET['app_id']) : (isset($_GET['id']) ? intval($_GET['id']) : 0);
        $app = talkgenai_get_app($app_id, true); // Use helper function with permission check
        
        if (!$app) {
            return;
        }
        
        // Parse and sanitize JSON spec (WordPress.org requirement)
        $json_spec_obj = null;
        if (!empty($app['json_spec'])) {
            $json_spec_raw = json_decode($app['json_spec'], true);
            // Sanitize decoded data (WordPress.org requirement)
            if (!is_array($json_spec_raw)) {
                $json_spec_obj = null;
            } else {
                $json_spec_obj = $this->sanitize_decoded_json($json_spec_raw);
            }
        }
        
        // Build inline script with app data
        $js = "
        jQuery(function($){
            // Debug: Log app data from PHP
            console.log('EDIT PAGE: App data from PHP:');
            console.log('App ID:', " . wp_json_encode($app['id']) . ");
            console.log('HTML content length:', " . wp_json_encode(strlen($app['html_content'] ?? '')) . ");
            console.log('JS content length:', " . wp_json_encode(strlen($app['js_content'] ?? '')) . ");
            console.log('JSON spec exists:', " . wp_json_encode(!empty($app['json_spec'])) . ");
            
            // Preload current app so admin.js switches to modify mode and updates preview
            window.currentAppData = {
                id: " . wp_json_encode($app['id']) . ",
                title: " . wp_json_encode($app['title']) . ",
                description: " . wp_json_encode($app['description'] ?? '') . ",
                html: " . wp_json_encode($app['html_content']) . ",
                css: " . wp_json_encode($app['css_content'] ?? '') . ",
                js: " . wp_json_encode($app['js_content'] ?? '') . ",
                json_spec: " . wp_json_encode($json_spec_obj ?: new stdClass()) . "
            };
            
            console.log('EDIT PAGE: window.currentAppData set:', window.currentAppData);

            try {
                // Show preview and execute JS using existing helpers
                if (typeof showPreview === 'function') {
                    showPreview(window.currentAppData.html || '', window.currentAppData.js || '', window.currentAppData.css || '');
                }
            } catch(e) {
                console.error('Error showing preview:', e);
            }
        });
        ";
        
        wp_add_inline_script('talkgenai-admin-base', $js);
    }
    
    /**
     * Get apps list page CSS
     */
    private function get_apps_list_page_css() {
        // CSS content extracted from inline <style> tag
        // This is required for admin page-specific styling scoped to .talkgenai-apps-page
        $css = file_get_contents(plugin_dir_path(__FILE__) . 'apps-list-page.css');
        return $css;
    }
    
    /**
     * Get apps list page JavaScript
     */
    private function get_apps_list_page_js() {
        // JavaScript content extracted from inline <script> tag
        // This handles app actions (view, edit, delete) and is page-specific
        
        // Build JS with PHP translations properly embedded
        $nonce = wp_create_nonce('talkgenai_bulk_action');
        
        $js = "
        jQuery(document).ready(function($) {
            // Detect RTL and add class for proper styling
            function detectRTL() {
                const isRTL = $('html').attr('dir') === 'rtl' || 
                             $('body').attr('dir') === 'rtl' || 
                             $('html').hasClass('rtl') ||
                             getComputedStyle(document.documentElement).direction === 'rtl';
                
                if (isRTL) {
                    $('.talkgenai-apps-page').addClass('talkgenai-rtl');
                }
                
                return isRTL;
            }
            
            // Initialize RTL detection
            const isRTL = detectRTL();
            

            // Bulk selection
            let selectedApps = [];
            
            $('.talkgenai-app-select').on('change', function() {
                const appId = $(this).val();
                if ($(this).is(':checked')) {
                    selectedApps.push(appId);
                } else {
                    selectedApps = selectedApps.filter(id => id !== appId);
                }
                updateBulkActions();
                updateSelectAllState();
            });

            // Select all functionality
            $('.talkgenai-select-all').on('change', function() {
                const isChecked = $(this).is(':checked');
                $('.talkgenai-app-select').prop('checked', isChecked);
                
                if (isChecked) {
                    selectedApps = [];
                    $('.talkgenai-app-select').each(function() {
                        selectedApps.push($(this).val());
                    });
                } else {
                    selectedApps = [];
                }
                updateBulkActions();
            });

            function updateSelectAllState() {
                const totalApps = $('.talkgenai-app-select').length;
                const selectedCount = selectedApps.length;
                
                if (selectedCount === 0) {
                    $('.talkgenai-select-all').prop('indeterminate', false).prop('checked', false);
                } else if (selectedCount === totalApps) {
                    $('.talkgenai-select-all').prop('indeterminate', false).prop('checked', true);
                } else {
                    $('.talkgenai-select-all').prop('indeterminate', true).prop('checked', false);
                }
            }

            function updateBulkActions() {
                const count = selectedApps.length;
                $('.talkgenai-selected-count').text(count + ' " . esc_js(esc_html__('selected', 'talkgenai')) . "');
                $('#talkgenai-bulk-action-selector').next('button').prop('disabled', count === 0);
            }

            // Handle bulk actions form submission
            $('#talkgenai-bulk-actions-form').on('submit', function(e) {
                const action = $('#talkgenai-bulk-action-selector').val();
                
                if (action === '-1') {
                    e.preventDefault();
                    alert('" . esc_js(esc_html__('Please select an action.', 'talkgenai')) . "');
                    return;
                }
                
                if (selectedApps.length === 0) {
                    e.preventDefault();
                    alert('" . esc_js(esc_html__('Please select at least one app.', 'talkgenai')) . "');
                    return;
                }
                
                if (action === 'delete') {
                    if (!confirm('" . esc_js(esc_html__('Are you sure you want to delete the selected apps? This action cannot be undone.', 'talkgenai')) . "')) {
                        e.preventDefault();
                        return;
                    }
                }
                
                // Add selected app IDs to the form
                $('.bulk-app-id').remove(); // Remove any existing hidden inputs
                selectedApps.forEach(function(appId) {
                    $(this).append('<input type=\"hidden\" name=\"app_ids[]\" value=\"' + appId + '\" class=\"bulk-app-id\">');
                }.bind(this));
            });

            // Copy shortcode functionality
            $('.talkgenai-copy-shortcode-btn').on('click', function() {
                const shortcode = $(this).data('shortcode');
                const btn = $(this);
                navigator.clipboard.writeText(shortcode).then(function() {
                    // Show success visual feedback
                    btn.addClass('copied');
                    setTimeout(function() {
                        btn.removeClass('copied');
                    }, 2000);
                });
            });

            // Delete app functionality (both old and new inline buttons)
            $('.delete-app, .talkgenai-delete-app-inline').on('click', function() {
                if (confirm('" . esc_js(esc_html__('Are you sure you want to delete this app?', 'talkgenai')) . "')) {
                    const appId = $(this).data('app-id');
                    
                    // Create a form and submit it to handle the deletion
                    const form = $('<form>', {
                        method: 'POST',
                        action: window.location.href
                    });
                    
                    form.append($('<input>', {
                        type: 'hidden',
                        name: 'action',
                        value: 'delete'
                    }));
                    
                    form.append($('<input>', {
                        type: 'hidden',
                        name: 'app_ids[]',
                        value: appId
                    }));
                    
                    form.append($('<input>', {
                        type: 'hidden',
                        name: 'talkgenai_bulk_nonce',
                        value: '" . esc_js($nonce) . "'
                    }));
                    
                    $('body').append(form);
                    form.submit();
                }
            });

            // Auto-submit filters on change
            $('.talkgenai-filter-select').on('change', function() {
                $('#talkgenai-apps-filter-form').submit();
            });

            // Search on Enter key press
            $('.talkgenai-search-input').on('keypress', function(e) {
                if (e.which === 13) { // Enter key
                    e.preventDefault();
                    $('#talkgenai-apps-filter-form').submit();
                }
            });
        });
        ";
        
        return $js;
    }
    
    /**
     * Register plugin settings
     */
    private function register_settings() {
        register_setting('talkgenai_settings_group', 'talkgenai_settings', array(
            'sanitize_callback' => array($this, 'sanitize_settings')
        ));
        
        // Add settings section
        add_settings_section(
            'talkgenai_main_section',
            __('Server Configuration', 'talkgenai'),
            array($this, 'settings_section_callback'),
            'talkgenai_settings'
        );
    }
    
    /**
     * Settings section callback
     */
    public function settings_section_callback() {
        echo '<p>' . esc_html__('Configure your TalkGenAI server connection settings.', 'talkgenai') . '</p>';
    }
    
    /**
     * Sanitize settings
     */
    public function sanitize_settings($settings) {
        // IMPORTANT: Do not call update_option() here to avoid recursive sanitize loop
        $validated = $this->api->validate_settings($settings);
        if (is_wp_error($validated)) {
            add_settings_error('talkgenai_settings', 'settings_error', $validated->get_error_message());
            return get_option('talkgenai_settings');
        }
        // Merge with current settings and let WP Settings API persist the returned array
        $current = get_option('talkgenai_settings', array());
        $updated = array_merge($current, $validated);
        
        // ✅ Check if API key was just added (was empty, now has value)
        $old_api_key = isset($current['remote_api_key']) ? $current['remote_api_key'] : '';
        $new_api_key = isset($updated['remote_api_key']) ? $updated['remote_api_key'] : '';
        
        if (empty($old_api_key) && !empty($new_api_key)) {
            // API key was just added - set transient to show success message
            set_transient('talkgenai_api_key_added', true, 30);
        }
        
        return $updated;
    }
    
    /**
     * Display admin notices
     */
    public function admin_notices() {
        // Debug logging removed for WordPress.org submission
        // if (defined('WP_DEBUG') && WP_DEBUG) {
        //     error_log('TalkGenAI admin_notices: Called');
        //     error_log('TalkGenAI admin_notices: is_admin = ' . (is_admin() ? 'true' : 'false'));
        // }
        
        // Check if on TalkGenAI pages
        if (!talkgenai_is_admin_page()) {
            // Debug logging removed for WordPress.org submission
            // if (defined('WP_DEBUG') && WP_DEBUG) {
            //     error_log('TalkGenAI admin_notices: Not on TalkGenAI page, exiting');
            // }
            return;
        }
        
        // ✅ Check if API key was just added
        if (get_transient('talkgenai_api_key_added')) {
            delete_transient('talkgenai_api_key_added');
            
            $generate_url = admin_url('admin.php?page=talkgenai');
            $message = sprintf(
                /* translators: %1$s: opening link tag, %2$s: closing link tag */
                __('🎉 <strong>Success!</strong> Your API key has been saved. You\'re now ready to generate apps! %1$sGo to Generate App%2$s and start building. Happy creating! 🚀', 'talkgenai'),
                '<a href="' . esc_url($generate_url) . '" class="button button-primary" style="margin-left: 10px;">',
                '</a>'
            );
            talkgenai_admin_notice($message, 'success');
        }
        
        // Debug logging removed for WordPress.org submission
        // if (defined('WP_DEBUG') && WP_DEBUG) {
        //     error_log('TalkGenAI admin_notices: On TalkGenAI page, checking API key');
        // }
        
        // Check if API key is configured
        $settings = $this->api->get_settings();
        // Debug logging removed for WordPress.org submission
        // if (defined('WP_DEBUG') && WP_DEBUG) {
        //     error_log('TalkGenAI admin_notices: API key = ' . (empty($settings['remote_api_key']) ? 'EMPTY' : 'SET'));
        //     error_log('TalkGenAI admin_notices: API key value = ' . var_export($settings['remote_api_key'], true));
        // }
        
        if (empty($settings['remote_api_key'])) {
            // Debug logging removed for WordPress.org submission
            // if (defined('WP_DEBUG') && WP_DEBUG) {
            //     error_log('TalkGenAI admin_notices: Showing API key notice');
            // }
            $settings_url = admin_url('admin.php?page=talkgenai-settings');
            $message = sprintf(
                /* translators: %1$s: opening link tag, %2$s: closing link tag, %3$s: opening settings link tag, %4$s: closing link tag */
                __('Welcome to TalkGenAI! To start generating apps, you need an API key. %1$sCreate your free account at TalkGenAI%2$s to get your API key, then %3$senter it in Settings%4$s.', 'talkgenai'),
                '<a href="https://app.talkgen.ai/" target="_blank" rel="noopener">',
                '</a>',
                '<a href="' . esc_url($settings_url) . '">',
                '</a>'
            );
            talkgenai_admin_notice($message, 'info');
        } elseif (!$this->api->is_server_configured()) {
            // Check general server configuration
            talkgenai_admin_notice(
                __('TalkGenAI server is not configured. Please configure your server settings.', 'talkgenai'),
                'warning'
            );
        }
    }
    
    /**
     * Render main admin page
     */
    public function render_main_page() {
        // Check user capability
        if (!current_user_can(TALKGENAI_MIN_CAPABILITY)) {
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'talkgenai'));
        }
        
        // Handle actions
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for page display only, capability check enforced
        $action = sanitize_key($_GET['action'] ?? 'generate');
        
        switch ($action) {
            case 'edit':
                $this->render_edit_app_page();
                break;
            case 'preview':
                $this->render_preview_app_page();
                break;
            default:
                $this->render_generate_app_page();
                break;
        }
    }
    
    /**
     * Render app generation page
     */
    private function render_generate_app_page() {
        // Check if user has API key
        $settings = get_option('talkgenai_settings', array());
        $api_key = isset($settings['remote_api_key']) ? $settings['remote_api_key'] : '';
        $has_api_key = !empty($api_key);
        
        $server_status = $this->api->get_server_status();
        $user_stats = $this->api->get_user_stats();
        $bonus_credits = 0;
        if (isset($user_stats['success']) && $user_stats['success'] && isset($user_stats['data']['bonus_credits'])) {
            $bonus_credits = intval($user_stats['data']['bonus_credits']);
        }
        
        // Get dashboard URL (filterable for testing)
        $dashboard_url = apply_filters('talkgenai_dashboard_url', 'https://app.talkgen.ai');
        $register_url = esc_url($dashboard_url);
        $settings_url = esc_url(admin_url('admin.php?page=talkgenai-settings'));
        ?>
        <div class="wrap">
            <div class="talkgenai-header-bar">
                <h1><?php esc_html_e('Generate App', 'talkgenai'); ?></h1>
                <div class="talkgenai-header-actions">
                    <div class="talkgenai-all-buttons">
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-settings')); ?>" class="button">
                            <span class="dashicons dashicons-admin-generic"></span>
                            <?php esc_html_e('Settings', 'talkgenai'); ?>
                        </a>
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-apps')); ?>" class="button">
                            <span class="dashicons dashicons-grid-view"></span>
                            <?php esc_html_e('View All Apps', 'talkgenai'); ?>
                        </a>
                        <button type="button" class="button" id="generate-new-btn-header" style="display: none;">
                            <span class="dashicons dashicons-plus-alt2"></span>
                            <?php esc_html_e('Generate New', 'talkgenai'); ?>
                        </button>
                    </div>
                    <?php if ($has_api_key): ?>
                    <div class="talkgenai-server-status-compact">
                        <?php if ($bonus_credits > 0): ?>
                            <span class="talkgenai-bonus-credits">
                                🎁 <strong><?php echo esc_html__('Bonus:', 'talkgenai'); ?></strong> <?php echo esc_html($bonus_credits); ?> <?php echo esc_html__('credits', 'talkgenai'); ?>
                            </span>
                        <?php endif; ?>
                        <?php echo wp_kses_post(talkgenai_get_server_status_indicator($server_status)); ?>
                        <span class="talkgenai-server-mode">
                            <?php echo talkgenai_is_local_server() ? esc_html__('Local', 'talkgenai') : esc_html__('Remote', 'talkgenai'); ?>
                        </span>
                    </div>
                    <?php endif; ?>
                </div>
            </div>
            
            <?php if (!$has_api_key): ?>
                <?php $this->render_no_api_key_setup('app'); ?>
            <?php else: ?>
                <!-- STATE 2: Has API Key - Show Full Interface -->
            
            <div class="talkgenai-admin-container">
                <div class="talkgenai-main-content">
                    <!-- Side-by-side workspace for generation form and preview -->
                    <div class="talkgenai-app-workspace">
                        <!-- Left side: Generation Form -->
                        <div class="talkgenai-generation-section">
                            <div class="talkgenai-generation-form">
                                <h3><?php esc_html_e('Describe Your App', 'talkgenai'); ?></h3>
                                <form id="talkgenai-generate-form">
                                    <div class="tgai-form-field">
                                        <label class="tgai-form-label" for="app_description">
                                            <?php esc_html_e('What would you like to build?', 'talkgenai'); ?>
                                        </label>
                                        <textarea
                                            id="app_description"
                                            name="description"
                                            rows="6"
                                            class="large-text"
                                            placeholder="<?php esc_attr_e('Describe the app you want to create...', 'talkgenai'); ?>"
                                            required
                                        ></textarea>
                                        <p class="description">
                                            <?php esc_html_e('For example: "Create a calculator with dark theme" or "Make a todo list with categories"', 'talkgenai'); ?>
                                        </p>
                                    </div>
                                    
                                    <p class="submit">
                                        <button type="submit" class="button button-primary" id="generate-app-btn">
                                            <span class="dashicons dashicons-update" style="display: none;"></span>
                                            <span class="dashicons dashicons-controls-play" id="generate-icon"></span>
                                            <span id="button-text"><?php esc_html_e('Generate App', 'talkgenai'); ?></span>
                                        </button>
                                    </p>
                                </form>
                                
                                <!-- Action buttons for generated apps -->
                                <div class="talkgenai-form-actions" style="display: none;">
                                    <button type="button" class="button button-primary" id="save-app-btn">
                                        <?php esc_html_e('Save App', 'talkgenai'); ?>
                                    </button>
                                    <button type="button" class="button" id="generate-new-btn">
                                        <?php esc_html_e('Generate New', 'talkgenai'); ?>
                                    </button>
                                </div>
                            </div>
                            
                            <!-- Additional Action Buttons -->
                            <div class="talkgenai-additional-actions">
                                <button type="button" class="button talkgenai-secondary-btn" id="get-app-ideas-btn">
                                    <?php esc_html_e('Get App Ideas', 'talkgenai'); ?>
                                </button>
                            </div>
                        </div>
                        
                        <!-- Right side: Preview Area -->
                        <div class="talkgenai-preview-section">
                            <div id="talkgenai-preview-area" style="display: none;">
                                <h3><?php esc_html_e('App Preview', 'talkgenai'); ?></h3>
                                <div id="talkgenai-preview-container">
                                    <!-- Generated app will be loaded here -->
                                </div>
                                
                            </div>
                            
                            <!-- Placeholder for when no app is generated yet -->
                            <div id="talkgenai-preview-placeholder" class="talkgenai-generation-form">
                                <h3><?php esc_html_e('Generated App Preview', 'talkgenai'); ?></h3>
                                <div class="tgai-placeholder-inner">
                                    <div class="tgai-placeholder-icon">
                                        <span class="dashicons dashicons-smartphone"></span>
                                    </div>
                                    <p class="tgai-placeholder-title"><?php esc_html_e('Describe your app and AI will generate it!', 'talkgenai'); ?></p>
                                    <p class="tgai-placeholder-subtitle"><?php esc_html_e('Your generated app will appear here for preview', 'talkgenai'); ?></p>

                                    <div class="tgai-examples-container">
                                        <p class="tgai-examples-title"><?php esc_html_e('Try these examples - click to generate:', 'talkgenai'); ?></p>

                                        <div class="tgai-examples-grid">
                                            <div class="talkgenai-example-card" data-example="Tip calculator">
                                                <div class="card-icon">
                                                    <span class="dashicons dashicons-calculator"></span>
                                                </div>
                                                <div class="card-content">
                                                    <div class="card-label"><?php esc_html_e('Calculator', 'talkgenai'); ?></div>
                                                    <div class="card-text">"Tip calculator"</div>
                                                </div>
                                            </div>

                                            <div class="talkgenai-example-card" data-example="Countdown to January 1, 2027">
                                                <div class="card-icon">
                                                    <span class="dashicons dashicons-clock"></span>
                                                </div>
                                                <div class="card-content">
                                                    <div class="card-label"><?php esc_html_e('Timer', 'talkgenai'); ?></div>
                                                    <div class="card-text">"Countdown to January 1, 2027"</div>
                                                </div>
                                            </div>

                                            <div class="talkgenai-example-card" data-example="Shopping list with checkboxes">
                                                <div class="card-icon">
                                                    <span class="dashicons dashicons-yes-alt"></span>
                                                </div>
                                                <div class="card-content">
                                                    <div class="card-label"><?php esc_html_e('To-Do List', 'talkgenai'); ?></div>
                                                    <div class="card-text">"Shopping list with checkboxes"</div>
                                                </div>
                                            </div>

                                            <div class="talkgenai-example-card" data-example="iPhone vs Android comparison table">
                                                <div class="card-icon">
                                                    <span class="dashicons dashicons-editor-table"></span>
                                                </div>
                                                <div class="card-content">
                                                    <div class="card-label"><?php esc_html_e('Comparison', 'talkgenai'); ?></div>
                                                    <div class="card-text">"iPhone vs Android comparison"</div>
                                                </div>
                                            </div>
                                        </div>

                                        <p class="tgai-examples-hint">
                                            <?php esc_html_e('Click any example above to auto-fill and generate instantly!', 'talkgenai'); ?>
                                        </p>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                
                <!-- Sidebar -->
                <div class="talkgenai-sidebar">
                    <div class="talkgenai-sidebar-box">
                        <h3><?php esc_html_e('Recent Apps', 'talkgenai'); ?></h3>
                        <?php $this->render_recent_apps(); ?>
                    </div>
                    
                </div>
            </div>
            <?php endif; // End has_api_key check ?>
        </div><!-- .wrap -->
        <?php
    }
    
    /**
     * Render recent apps in sidebar
     */
    private function render_recent_apps() {
        $recent_apps = $this->database->get_user_apps(get_current_user_id(), 'active', 5);
        
        if (empty($recent_apps)) {
            echo '<p>' . esc_html__('No apps created yet.', 'talkgenai') . '</p>';
            return;
        }
        
        echo '<ul class="talkgenai-recent-apps">';
        foreach ($recent_apps as $app) {
            $formatted_app = talkgenai_format_app_data($app);
            printf(
                '<li><a href="%s" title="%s">%s</a> <small>(%s)</small></li>',
                esc_url(talkgenai_get_app_edit_url($app['id'])),
                esc_attr($formatted_app['description']),
                esc_html($formatted_app['title']),
                esc_html($formatted_app['created_at'])
            );
        }
        echo '</ul>';
    }
    
    /**
     * Render apps management page
     */
    public function render_apps_page() {
        if (!current_user_can(TALKGENAI_MIN_CAPABILITY)) {
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'talkgenai'));
        }
        
        // Handle bulk actions
        $this->handle_bulk_actions();
        
        // Get filter parameters
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin page URL parameters for filtering display only
        $search = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin page URL parameters for filtering display only
        $filter_type = isset($_GET['filter_type']) ? sanitize_text_field(wp_unslash($_GET['filter_type'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin page URL parameters for filtering display only
        $filter_scope = isset($_GET['filter_scope']) ? sanitize_text_field(wp_unslash($_GET['filter_scope'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Admin page URL parameters for filtering display only
        $filter_active = isset($_GET['filter_active']) ? sanitize_text_field(wp_unslash($_GET['filter_active'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for filtering display only
        $filter_status = sanitize_key($_GET['filter_status'] ?? 'active');
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for sorting display only
        $sort_by = sanitize_key($_GET['sort_by'] ?? 'created_at');
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for sorting display only
        $sort_order = sanitize_key($_GET['sort_order'] ?? 'desc');
        
        // Get apps with filters
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for pagination display only
        $page = intval($_GET['paged'] ?? 1);
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for pagination display only
        $per_page = intval($_GET['per_page'] ?? 12);
        $offset = ($page - 1) * $per_page;
        
        $apps = $this->get_filtered_apps(get_current_user_id(), $search, $filter_type, $filter_scope, $filter_active, $filter_status, $sort_by, $sort_order, $per_page, $offset);
        $total_apps = $this->get_filtered_apps_count(get_current_user_id(), $search, $filter_type, $filter_scope, $filter_active, $filter_status);
        
        // Get unique values for filters
        $app_types = $this->get_unique_app_types(get_current_user_id());
        $scopes = $this->get_unique_scopes(get_current_user_id());
        
        ?>
        <div class="wrap talkgenai-apps-page">
            <!-- Modern Header -->
            <div class="talkgenai-apps-header">
                <div class="talkgenai-apps-header-content">
                    <div class="talkgenai-apps-title-section">
                        <h1 class="talkgenai-apps-title">
                            <span class="dashicons dashicons-admin-page"></span>
                            <?php esc_html_e('My Apps', 'talkgenai'); ?>
                            <span class="talkgenai-apps-count"><?php echo absint($total_apps); ?></span>
                        </h1>
                        <p class="talkgenai-apps-subtitle"><?php esc_html_e('Manage your AI-generated applications', 'talkgenai'); ?></p>
                    </div>
                    <div class="talkgenai-apps-actions">
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai')); ?>" class="talkgenai-btn talkgenai-btn-primary">
                            <span class="dashicons dashicons-plus-alt2"></span>
                <?php esc_html_e('Generate New App', 'talkgenai'); ?>
            </a>
                    </div>
                </div>
            </div>

            <!-- Advanced Filters & Search -->
            <div class="talkgenai-apps-filters">
                <form method="get" id="talkgenai-apps-filter-form" class="talkgenai-filters-form">
                    <input type="hidden" name="page" value="talkgenai-apps">
                    
                    <!-- Search Bar -->
                    <div class="talkgenai-search-container">
                        <div class="talkgenai-search-box">
                            <span class="dashicons dashicons-search"></span>
                            <input type="text" 
                                   name="s" 
                                   value="<?php echo esc_attr($search); ?>" 
                                   placeholder="<?php esc_attr_e('Search apps by title or description...', 'talkgenai'); ?>"
                                   class="talkgenai-search-input">
                            <?php if ($search): ?>
                                <button type="button" class="talkgenai-search-clear" onclick="this.previousElementSibling.value=''; this.form.submit();">
                                    <span class="dashicons dashicons-no-alt"></span>
                                </button>
                            <?php endif; ?>
                        </div>
                    </div>

                    <!-- Filter Controls -->
                    <div class="talkgenai-filters-row">
                        <div class="talkgenai-filter-group">
                            <label><?php esc_html_e('Type', 'talkgenai'); ?></label>
                            <select name="filter_type" class="talkgenai-filter-select">
                                <option value=""><?php esc_html_e('All Types', 'talkgenai'); ?></option>
                                <?php foreach ($app_types as $type): ?>
                                    <option value="<?php echo esc_attr($type['app_type']); ?>" <?php selected($filter_type, $type['app_type']); ?>>
                                        <?php echo esc_html(talkgenai_get_app_type_name($type['app_class'], $type['app_type'])); ?>
                                        <span class="count">(<?php echo absint($type['count']); ?>)</span>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>

                        <div class="talkgenai-filter-group">
                            <label><?php esc_html_e('Scope', 'talkgenai'); ?></label>
                            <select name="filter_scope" class="talkgenai-filter-select">
                                <option value=""><?php esc_html_e('All Scopes', 'talkgenai'); ?></option>
                                <?php foreach ($scopes as $scope): ?>
                                    <option value="<?php echo esc_attr($scope['scope']); ?>" <?php selected($filter_scope, $scope['scope']); ?>>
                                        <?php echo esc_html($scope['scope']); ?>
                                        <span class="count">(<?php echo absint($scope['count']); ?>)</span>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>

                        <div class="talkgenai-filter-group">
                            <label><?php esc_html_e('Status', 'talkgenai'); ?></label>
                            <select name="filter_active" class="talkgenai-filter-select">
                                <option value=""><?php esc_html_e('All Status', 'talkgenai'); ?></option>
                                <option value="1" <?php selected($filter_active, '1'); ?>><?php esc_html_e('Active', 'talkgenai'); ?></option>
                                <option value="0" <?php selected($filter_active, '0'); ?>><?php esc_html_e('Inactive', 'talkgenai'); ?></option>
                            </select>
                        </div>

                        <div class="talkgenai-filter-group">
                            <label><?php esc_html_e('Sort By', 'talkgenai'); ?></label>
                            <select name="sort_by" class="talkgenai-filter-select">
                                <option value="created_at" <?php selected($sort_by, 'created_at'); ?>><?php esc_html_e('Date Created', 'talkgenai'); ?></option>
                                <option value="updated_at" <?php selected($sort_by, 'updated_at'); ?>><?php esc_html_e('Last Modified', 'talkgenai'); ?></option>
                                <option value="title" <?php selected($sort_by, 'title'); ?>><?php esc_html_e('Title', 'talkgenai'); ?></option>
                                <option value="app_type" <?php selected($sort_by, 'app_type'); ?>><?php esc_html_e('Type', 'talkgenai'); ?></option>
                            </select>
                        </div>

                        <div class="talkgenai-filter-group">
                            <label><?php esc_html_e('Order', 'talkgenai'); ?></label>
                            <select name="sort_order" class="talkgenai-filter-select">
                                <option value="desc" <?php selected($sort_order, 'desc'); ?>><?php esc_html_e('Descending', 'talkgenai'); ?></option>
                                <option value="asc" <?php selected($sort_order, 'asc'); ?>><?php esc_html_e('Ascending', 'talkgenai'); ?></option>
                            </select>
                        </div>

                        <div class="talkgenai-filter-actions">
                            <button type="submit" class="talkgenai-btn talkgenai-btn-secondary">
                                <span class="dashicons dashicons-filter"></span>
                                <?php esc_html_e('Apply Filters', 'talkgenai'); ?>
                            </button>
                            <?php if ($search || $filter_type || $filter_scope || $filter_active || $sort_by !== 'created_at' || $sort_order !== 'desc'): ?>
                                <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-apps')); ?>" class="talkgenai-btn talkgenai-btn-ghost">
                                    <?php esc_html_e('Clear All', 'talkgenai'); ?>
                                </a>
                            <?php endif; ?>
                        </div>
                    </div>
                </form>
            </div>
            
            <?php if (empty($apps)): ?>
                <div class="talkgenai-empty-state">
                    <?php if ($search || $filter_type || $filter_scope || $filter_active): ?>
                        <div class="talkgenai-empty-icon">
                            <span class="dashicons dashicons-search"></span>
                        </div>
                        <h2><?php esc_html_e('No apps match your filters', 'talkgenai'); ?></h2>
                        <p><?php esc_html_e('Try adjusting your search criteria or filters to find what you\'re looking for.', 'talkgenai'); ?></p>
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-apps')); ?>" class="talkgenai-btn talkgenai-btn-secondary">
                            <?php esc_html_e('Clear Filters', 'talkgenai'); ?>
                        </a>
                    <?php else: ?>
                        <div class="talkgenai-empty-icon">
                            <span class="dashicons dashicons-admin-page"></span>
                        </div>
                    <h2><?php esc_html_e('No apps created yet', 'talkgenai'); ?></h2>
                        <p><?php esc_html_e('Create your first AI-generated app to get started with TalkGenAI.', 'talkgenai'); ?></p>
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai')); ?>" class="talkgenai-btn talkgenai-btn-primary">
                            <span class="dashicons dashicons-plus-alt2"></span>
                        <?php esc_html_e('Generate Your First App', 'talkgenai'); ?>
                    </a>
                    <?php endif; ?>
                </div>
            <?php else: ?>
                <!-- View Toggle & Per Page -->
                <div class="talkgenai-apps-controls">
                    <div class="talkgenai-view-controls">
                        <div class="talkgenai-per-page">
                            <label><?php esc_html_e('Show:', 'talkgenai'); ?></label>
                            <select name="per_page" onchange="location.href=this.value">
                                <?php
                                $per_page_options = [6, 12, 24, 48];
                                foreach ($per_page_options as $option) {
                                    $url = add_query_arg(['per_page' => $option, 'paged' => 1]);
                                    echo '<option value="' . esc_url($url) . '"' . selected($per_page, $option, false) . '>' . absint($option) . '</option>';
                                }
                                ?>
                            </select>
                        </div>
                    </div>
                    
                    <!-- Bulk Actions -->
                    <form method="post" id="talkgenai-bulk-actions-form">
                        <?php wp_nonce_field('talkgenai_bulk_action', 'talkgenai_bulk_nonce'); ?>
                        <div class="talkgenai-bulk-actions">
                            <select name="action" id="talkgenai-bulk-action-selector">
                                <option value="-1"><?php esc_html_e('Bulk Actions', 'talkgenai'); ?></option>
                                <option value="delete"><?php esc_html_e('Delete Selected', 'talkgenai'); ?></option>
                            </select>
                            <button type="submit" class="talkgenai-btn talkgenai-btn-secondary" disabled>
                                <?php esc_html_e('Apply', 'talkgenai'); ?>
                            </button>
                            <span class="talkgenai-selected-count">0 <?php esc_html_e('selected', 'talkgenai'); ?></span>
                        </div>
                    </form>
                        </div>
                        
                <!-- Apps Grid/List -->
                <div class="talkgenai-apps-container talkgenai-list-view">
                    <!-- Table Header for List View -->
                    <div class="talkgenai-list-header">
                        <div class="talkgenai-list-header-row">
                            <div class="talkgenai-header-cell talkgenai-header-select">
                                <input type="checkbox" id="select-all-apps" class="talkgenai-select-all">
                            </div>
                            <div class="talkgenai-header-cell talkgenai-header-app"><?php esc_html_e('App', 'talkgenai'); ?></div>
                            <div class="talkgenai-header-cell talkgenai-header-type"><?php esc_html_e('Type', 'talkgenai'); ?></div>
                            <div class="talkgenai-header-cell talkgenai-header-scope"><?php esc_html_e('Scope', 'talkgenai'); ?></div>
                            <div class="talkgenai-header-cell talkgenai-header-shortcode"><?php esc_html_e('Shortcode', 'talkgenai'); ?></div>
                            <div class="talkgenai-header-cell talkgenai-header-date"><?php esc_html_e('Created', 'talkgenai'); ?></div>
                            <div class="talkgenai-header-cell talkgenai-header-status"><?php esc_html_e('Status', 'talkgenai'); ?></div>
                        </div>
                    </div>
                    
                            <?php foreach ($apps as $app): 
                                $formatted_app = talkgenai_format_app_data($app);
                        $active_value = isset($app['active']) ? intval($app['active']) : 1;
                    ?>
                        <div class="talkgenai-app-card" data-app-id="<?php echo absint($app['id']); ?>">
                            <!-- Checkbox Column -->
                            <div class="talkgenai-app-card-select">
                                <input type="checkbox" name="app_ids[]" value="<?php echo absint($app['id']); ?>" class="talkgenai-app-select">
                            </div>

                            <!-- App Column -->
                            <div class="talkgenai-app-card-body">
                                <div class="talkgenai-app-icon">
                                    <span class="dashicons <?php echo esc_attr(talkgenai_get_app_class_icon($app['app_class'])); ?>"></span>
                                </div>
                                <div class="talkgenai-app-content">
                                    <h3 class="talkgenai-app-title">
                                            <a href="<?php echo esc_url(talkgenai_get_app_edit_url($app['id'])); ?>">
                                                <?php echo esc_html($formatted_app['title']); ?>
                                            </a>
                                    </h3>
                                    <p class="talkgenai-app-description">
                                        <?php echo esc_html(wp_trim_words($app['description'] ?? '', 15)); ?>
                                    </p>
                                    <div class="talkgenai-app-actions-inline">
                                        <a href="<?php echo esc_url(talkgenai_get_app_edit_url($app['id'])); ?>" class="talkgenai-btn talkgenai-btn-small talkgenai-btn-primary">
                                                    <?php esc_html_e('Edit', 'talkgenai'); ?>
                                        </a>
                                        <a href="<?php echo esc_url(talkgenai_get_app_preview_url($app['id'])); ?>" class="talkgenai-btn talkgenai-btn-small talkgenai-btn-secondary">
                                                    <?php esc_html_e('Preview', 'talkgenai'); ?>
                                                </a>
                                        <button type="button" class="talkgenai-btn talkgenai-btn-small talkgenai-btn-danger delete-app" data-app-id="<?php echo absint($app['id']); ?>">
                                            <?php esc_html_e('Delete', 'talkgenai'); ?>
                                        </button>
                                        </div>
                                </div>
                            </div>

                            <!-- Type Column -->
                            <div class="talkgenai-app-type-col">
                                <span class="talkgenai-meta-value"><?php echo esc_html(talkgenai_get_app_type_name($app['app_class'], $app['app_type'])); ?></span>
                            </div>

                            <!-- Scope Column -->
                            <div class="talkgenai-app-scope-col">
                                <span class="talkgenai-meta-value"><?php echo esc_html($app['scope'] ?? 'content'); ?></span>
                            </div>

                            <!-- Shortcode Column -->
                            <div class="talkgenai-app-shortcode-col">
                                <div class="talkgenai-shortcode-container">
                                    <code class="talkgenai-shortcode-text">[talkgenai_app id="<?php echo esc_attr($app['id']); ?>"]</code>
                                    <button type="button" class="talkgenai-copy-shortcode-btn" data-shortcode='[talkgenai_app id="<?php echo esc_attr($app['id']); ?>"]' title="<?php esc_html_e('Copy shortcode', 'talkgenai'); ?>">
                                        <span class="dashicons dashicons-admin-page"></span>
                                    </button>
                                </div>
                            </div>

                            <!-- Created Column -->
                            <div class="talkgenai-app-date-col">
                                <span class="dashicons dashicons-calendar-alt"></span>
                                        <?php echo esc_html($formatted_app['created_at']); ?>
                            </div>

                            <!-- Status Column -->
                            <div class="talkgenai-app-status-col">
                                <span class="talkgenai-status-badge <?php echo esc_attr($active_value ? 'active' : 'inactive'); ?>">
                                    <?php echo $active_value ? esc_html__('Active', 'talkgenai') : esc_html__('Inactive', 'talkgenai'); ?>
                                </span>
                            </div>

                        </div>
                            <?php endforeach; ?>
                </div>

                <!-- Enhanced Pagination -->
                <?php if ($total_apps > $per_page): ?>
                    <div class="talkgenai-pagination">
                        <?php
                        $total_pages = ceil($total_apps / $per_page);
                        $pagination_args = array(
                            'base' => add_query_arg('paged', '%#%'),
                            'format' => '',
                            'prev_text' => '<span class="dashicons dashicons-arrow-left-alt2"></span>' . esc_html__('Previous', 'talkgenai'),
                            'next_text' => esc_html__('Next', 'talkgenai') . '<span class="dashicons dashicons-arrow-right-alt2"></span>',
                            'total' => $total_pages,
                            'current' => $page,
                            'show_all' => false,
                            'end_size' => 1,
                            'mid_size' => 2,
                            'type' => 'array'
                        );
                        
                        $pagination_links = paginate_links($pagination_args);
                        
                        if ($pagination_links) {
                            echo '<div class="talkgenai-pagination-wrapper">';
                            echo '<div class="talkgenai-pagination-info">';
                            printf(
                                /* translators: 1: first item number on page, 2: last item number on page, 3: total items */
                                esc_html__('Showing %1$d-%2$d of %3$d apps', 'talkgenai'),
                                absint((($page - 1) * $per_page) + 1),
                                absint(min($page * $per_page, $total_apps)),
                                absint($total_apps)
                            );
                            echo '</div>';
                            echo '<div class="talkgenai-pagination-links">';
                            foreach ($pagination_links as $link) {
                                // Pagination links from paginate_links() - sanitized with wp_kses_post()
                                echo wp_kses_post($link);
                            }
                            echo '</div>';
                            echo '</div>';
                        }
                        ?>
                    </div>
                <?php endif; ?>
            <?php endif; ?>
        </div>

        <?php
        // Note: Styles are now properly enqueued using wp_add_inline_style() in enqueue_admin_assets()
        // This follows WordPress best practices for including page-specific CSS
        ?>

        <?php
        // Styles and scripts are now loaded via wp_add_inline_style() and wp_add_inline_script()
        // in enqueue_admin_assets() following WordPress best practices
        ?>
        <?php
    }
    
    /**
     * Render the "no API key" setup guide (shared by app and article pages).
     *
     * @param string $context 'app' or 'article' — adjusts the headline and footer line.
     */
    private function render_no_api_key_setup($context = 'app') {
        $register_url = esc_url(apply_filters('talkgenai_dashboard_url', 'https://app.talkgen.ai'));
        $settings_url = esc_url(admin_url('admin.php?page=talkgenai-settings'));

        if ($context === 'article') {
            $icon     = '✍️';
            $headline = __('Start Generating SEO Articles in Minutes', 'talkgenai');
            $subtitle = __('AI-written, SEO-optimized articles with internal links, FAQ sections, and your brand voice — posted straight to WordPress.', 'talkgenai');
            $footer   = __('Free: 10 credits/month &bull; Article generation &bull; SEO-optimized', 'talkgenai');
        } else {
            $icon     = '⚡';
            $headline = __('Give Your WordPress Site AI Superpowers', 'talkgenai');
            $subtitle = __('Create AI-powered calculators, converters, and interactive tools in seconds.', 'talkgenai');
            $footer   = __('Free: 10 credits/month &bull; 5 active apps &bull; WordPress plugin', 'talkgenai');
        }
        ?>
        <div class="talkgenai-empty-state-container">
            <div class="talkgenai-empty-state">
                <div class="empty-state-icon"><?php echo esc_html($icon); ?></div>

                <h2><?php echo esc_html($headline); ?></h2>

                <p class="empty-state-subtitle"><?php echo esc_html($subtitle); ?></p>

                <div class="setup-steps">
                    <h3><?php esc_html_e('Get your free API key in 3 steps:', 'talkgenai'); ?></h3>

                    <ol class="step-list">
                        <li>
                            <span class="step-number">1️⃣</span>
                            <span class="step-text">
                                <?php
                                printf(
                                    /* translators: %1$s opening link, %2$s closing link */
                                    esc_html__('Go to %1$sapp.talkgen.ai%2$s and create a free account (takes 30 seconds)', 'talkgenai'),
                                    '<a href="' . esc_url( $register_url ) . '" target="_blank" rel="noopener noreferrer"><strong>',
                                    '</strong></a>'
                                );
                                ?>
                            </span>
                        </li>
                        <li>
                            <span class="step-number">2️⃣</span>
                            <span class="step-text">
                                <?php esc_html_e('In the dashboard, open the ', 'talkgenai'); ?>
                                <strong><?php esc_html_e('Integrations', 'talkgenai'); ?></strong>
                                <?php esc_html_e(' tab and copy your WordPress API key', 'talkgenai'); ?>
                            </span>
                        </li>
                        <li>
                            <span class="step-number">3️⃣</span>
                            <span class="step-text">
                                <?php esc_html_e('Come back here, open ', 'talkgenai'); ?>
                                <a href="<?php echo esc_url( $settings_url ); ?>"><strong><?php esc_html_e('Settings', 'talkgenai'); ?></strong></a>
                                <?php esc_html_e(' and paste the key into the ', 'talkgenai'); ?>
                                <strong><?php esc_html_e('Remote API Key', 'talkgenai'); ?></strong>
                                <?php esc_html_e(' field', 'talkgenai'); ?>
                            </span>
                        </li>
                    </ol>
                </div>

                <div class="empty-state-actions">
                    <a href="<?php echo esc_url( $register_url ); ?>"
                       class="button button-primary button-hero"
                       target="_blank"
                       rel="noopener noreferrer">
                        <?php esc_html_e('Get Started Free', 'talkgenai'); ?>
                    </a>

                    <a href="<?php echo esc_url( $settings_url ); ?>"
                       class="button button-secondary button-hero">
                        <?php esc_html_e('I Already Have a Key', 'talkgenai'); ?>
                    </a>
                </div>

                <div class="empty-state-footer">
                    <p class="social-proof">✓ <?php echo wp_kses_post($footer); ?></p>
                </div>
            </div>
        </div>
        <?php
    }

    /**
     * Render articles generation page
     */
    public function render_articles_page() {
        // Clear cached stats so plan changes take effect immediately
        $this->api->clear_stats_cache();
        $user_stats = $this->api->get_user_stats();
        $user_plan = 'free';
        if (isset($user_stats['success']) && $user_stats['success'] && isset($user_stats['data']['plan'])) {
            $user_plan = $user_stats['data']['plan'];
        }

        $bonus_credits = 0;
        if (isset($user_stats['success']) && $user_stats['success'] && isset($user_stats['data']['bonus_credits'])) {
            $bonus_credits = intval($user_stats['data']['bonus_credits']);
        }
        $is_free = ($user_plan === 'free' && $bonus_credits <= 0);

        $article_settings = get_option('talkgenai_settings', array());
        $has_api_key = !empty($article_settings['remote_api_key']);
        ?>
        <div class="wrap">
            <h1><?php esc_html_e('Generate Articles', 'talkgenai'); ?></h1>

            <?php if (!$has_api_key) : ?>
                <?php $this->render_no_api_key_setup('article'); ?>
            <?php else : ?>
            <div class="talkgenai-admin-container">
                <div class="talkgenai-main-content">
                    <div class="talkgenai-generation-form">
                        <h3><?php esc_html_e('Generate Article', 'talkgenai'); ?></h3>

                        <form id="talkgenai-unified-article-form">

                            <!-- Article Source Pill Toggle -->
                            <div class="tgai-field-group">
                                <div class="tgai-pill-toggle" id="tgai-source-toggle">
                                    <span class="tgai-pill-toggle__slider"></span>
                                    <label class="tgai-pill-toggle__option tgai-pill-toggle__option--active">
                                        <input type="radio" name="article_source" value="standalone" checked />
                                        <span class="dashicons dashicons-media-document"></span>
                                        <?php esc_html_e('Standalone', 'talkgenai'); ?>
                                    </label>
                                    <label class="tgai-pill-toggle__option">
                                        <input type="radio" name="article_source" value="app-based" />
                                        <span class="dashicons dashicons-admin-generic"></span>
                                        <?php esc_html_e('App-Based', 'talkgenai'); ?>
                                    </label>
                                </div>
                            </div>

                            <!-- App Selection (shown only for App-Based) -->
                            <div class="tgai-field-group talkgenai-app-based-field" style="display: none;">
                                <label class="tgai-field-label" for="target_app"><?php esc_html_e('Select App', 'talkgenai'); ?></label>
                                <select id="target_app" name="app_id" class="regular-text tgai-select2-app-selector" style="width: 100%;">
                                    <option value=""><?php esc_html_e('Choose an app...', 'talkgenai'); ?></option>
                                    <?php
                                    $current_user_id = get_current_user_id();
                                    $apps = $this->database->get_user_apps($current_user_id, 'active', 200);
                                    if (empty($apps)) {
                                        global $wpdb;
                                        $table_name = esc_sql($this->database->get_apps_table());
                                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for fallback testing scenario when user has no apps
                                        $apps = $wpdb->get_results(
                                            $wpdb->prepare(
                                                "SELECT id, title, description, json_spec FROM " . esc_sql($table_name) . " WHERE status = %s ORDER BY created_at DESC LIMIT 100",
                                                'active'
                                            ),
                                            ARRAY_A
                                        );
                                    }
                                    if (!empty($apps)) {
                                        foreach ($apps as $app) {
                                            $title = esc_html($app['title'] ?? 'Untitled App');
                                            $id = absint($app['id']);
                                            echo '<option value="' . absint($id) . '">' . esc_html($title) . '</option>';
                                        }
                                    } else {
                                        echo "<option value=\"\" disabled>No apps found. Please generate an app first.</option>";
                                        if (defined('WP_DEBUG') && WP_DEBUG) {
                                            echo '<option value="" disabled>' . esc_html('Debug: User ID = ' . absint($current_user_id)) . '</option>';
                                        }
                                    }
                                    ?>
                                </select>
                            </div>

                            <!-- Row 1: Article Title + Article Length side by side -->
                            <div class="tgai-form-2col">
                                <div class="tgai-field-group tgai-col-grow">
                                    <label class="tgai-field-label" for="article_title"><?php esc_html_e('Article Title', 'talkgenai'); ?> <span class="required">*</span></label>
                                    <input type="text" id="article_title" name="article_title" class="tgai-input" required placeholder="<?php esc_html_e('Enter the title of your article...', 'talkgenai'); ?>" />
                                </div>
                                <div class="tgai-field-group tgai-col-auto">
                                    <label class="tgai-field-label"><?php esc_html_e('Length', 'talkgenai'); ?></label>
                                    <select id="article_length" name="length" class="regular-text" style="display: none;">
                                        <option value="short" <?php selected($is_free); ?>><?php esc_html_e('Short', 'talkgenai'); ?></option>
                                        <option value="medium" <?php selected(!$is_free); ?>><?php esc_html_e('Medium', 'talkgenai'); ?></option>
                                        <option value="long"><?php esc_html_e('Long', 'talkgenai'); ?></option>
                                    </select>
                                    <div class="tgai-length-pills">
                                        <div class="tgai-length-pill<?php echo $is_free ? ' active' : ''; ?>" data-value="short">
                                            <span class="tgai-length-pill__label"><?php esc_html_e('Short', 'talkgenai'); ?></span>
                                        </div>
                                        <div class="tgai-length-premium-group">
                                            <div class="tgai-length-premium-group__pills">
                                                <div class="tgai-length-pill<?php echo $is_free ? ' tgai-premium-locked' : ' active'; ?>" data-value="medium">
                                                    <span class="tgai-length-pill__label"><?php esc_html_e('Medium', 'talkgenai'); ?></span>
                                                </div>
                                                <div class="tgai-length-pill<?php echo $is_free ? ' tgai-premium-locked' : ''; ?>" data-value="long">
                                                    <span class="tgai-length-pill__label"><?php esc_html_e('Long', 'talkgenai'); ?></span>
                                                </div>
                                            </div>
                                            <?php if ($is_free) : ?>
                                                <a href="https://app.talkgen.ai/" target="_blank" class="tgai-premium-link tgai-premium-link--centered"><span class="tgai-badge--premium">PREMIUM</span></a>
                                            <?php endif; ?>
                                        </div>
                                    </div>
                                </div>
                            </div>

                            <!-- Describe Your Article -->
                            <div class="tgai-field-group">
                                <label class="tgai-field-label" for="article_topic"><?php esc_html_e('Describe Your Article', 'talkgenai'); ?></label>
                                <textarea id="article_topic" name="topic" class="tgai-textarea" rows="3" placeholder="<?php esc_html_e('What should this article cover? Include tone, keywords, sections, or any specific instructions...', 'talkgenai'); ?>"></textarea>
                            </div>

                            <!-- Section Divider -->
                            <hr class="tgai-section-divider" />

                            <!-- SEO & Features -->
                            <div class="tgai-collapsible" id="tgai-seo-section">
                                <div class="tgai-collapsible__header">
                                    <h4 class="tgai-collapsible__title">
                                        <span class="dashicons dashicons-admin-links"></span>
                                        <?php esc_html_e('SEO & Features', 'talkgenai'); ?>
                                    </h4>
                                    <span class="dashicons dashicons-arrow-down-alt2 tgai-collapsible__arrow"></span>
                                </div>
                                <div class="tgai-collapsible__body">

                                    <!-- Three toggles in one compact row -->
                                    <div class="tgai-toggles-inline">
                                        <div class="tgai-toggle-compact">
                                            <label class="tgai-toggle-switch tgai-toggle-switch--sm">
                                                <input type="checkbox" id="auto_internal_links" name="auto_internal_links" value="1" checked />
                                                <span class="tgai-toggle-switch__track"></span>
                                            </label>
                                            <span class="tgai-toggle-compact__label"><?php esc_html_e('Internal Links', 'talkgenai'); ?></span>
                                        </div>
                                        <div class="tgai-toggle-compact">
                                            <label class="tgai-toggle-switch tgai-toggle-switch--sm">
                                                <input type="checkbox" id="include_external_link" name="include_external_link" value="1" checked />
                                                <span class="tgai-toggle-switch__track"></span>
                                            </label>
                                            <span class="tgai-toggle-compact__label"><?php esc_html_e('External Link', 'talkgenai'); ?></span>
                                        </div>
                                        <div class="tgai-toggle-compact">
                                            <label class="tgai-toggle-switch tgai-toggle-switch--sm">
                                                <input type="checkbox" id="include_faq" name="include_faq" value="1" checked />
                                                <span class="tgai-toggle-switch__track"></span>
                                            </label>
                                            <span class="tgai-toggle-compact__label"><?php esc_html_e('FAQ Section', 'talkgenai'); ?></span>
                                        </div>
                                    </div>
                                    <?php if ($is_free) : ?>
                                        <p class="tgai-free-hint"><?php esc_html_e('Free plan uses a basic AI model — link placement may vary.', 'talkgenai'); ?> <a href="https://app.talkgen.ai/" target="_blank"><?php esc_html_e('Upgrade for premium AI models', 'talkgenai'); ?></a></p>
                                    <?php endif; ?>

                                    <!-- Generate Image toggle -->
                                    <div class="tgai-toggles-inline" style="margin-top: 8px;">
                                        <div class="tgai-toggle-compact">
                                            <label class="tgai-toggle-switch tgai-toggle-switch--sm">
                                                <input type="checkbox" id="create_image" name="create_image" value="1" <?php echo $is_free ? 'disabled' : 'checked'; ?> />
                                                <span class="tgai-toggle-switch__track"></span>
                                            </label>
                                            <span class="tgai-toggle-compact__label">
                                                <?php esc_html_e('Generate Image', 'talkgenai'); ?>
                                                <?php if ($is_free) : ?>
                                                    <a href="https://app.talkgen.ai/" target="_blank" class="tgai-badge--premium">PREMIUM</a>
                                                <?php endif; ?>
                                            </span>
                                        </div>
                                    </div>
                                    <?php if (!$is_free) : ?>
                                        <p class="tgai-free-hint"><?php esc_html_e('HD image (16:9, no text) generated alongside your article and inserted before the second heading.', 'talkgenai'); ?></p>
                                    <?php endif; ?>

                                    <!-- Manual URLs -->
                                    <div id="manual_urls_section" class="tgai-nested-field">
                                        <label class="tgai-field-label" for="internal_urls"><?php esc_html_e('Additional URLs (optional)', 'talkgenai'); ?></label>
                                        <textarea id="internal_urls" name="internal_urls" class="tgai-textarea tgai-textarea--short" rows="2" placeholder="https://example.com/page|Click Here&#10;https://youtube.com/watch?v=ID"></textarea>
                                    </div>

                                    <!-- App Page URL (conditional, premium) -->
                                    <div id="app-url-row" class="talkgenai-app-based-field" style="display: none; padding-top: 8px;">
                                        <label class="tgai-field-label" for="app_url">
                                            <?php esc_html_e('App Page URL', 'talkgenai'); ?>
                                            <span class="tgai-badge--premium">PREMIUM</span>
                                        </label>
                                        <input type="url" id="app_url" name="app_url" class="tgai-input" placeholder="https://yourdomain.com/my-app-page/" />
                                    </div>

                                </div>
                            </div>

                            <?php if ($is_free) : ?>
                            <!-- Brand Voice — premium upsell for free users -->
                            <div class="tgai-field-group">
                                <label class="tgai-field-label">
                                    <span class="dashicons dashicons-microphone" style="vertical-align: middle;"></span>
                                    <?php esc_html_e('Brand Voice', 'talkgenai'); ?>
                                    <a href="https://app.talkgen.ai/" target="_blank" class="tgai-badge--premium" style="margin-left:6px;">PREMIUM</a>
                                </label>
                                <p class="tgai-free-hint" style="margin-top:2px;">
                                    <?php esc_html_e('Teach TalkGenAI to write in your brand\'s tone. Articles will sound like they were written by your own team.', 'talkgenai'); ?>
                                    <a href="https://app.talkgen.ai/" target="_blank"><?php esc_html_e('Upgrade to unlock →', 'talkgenai'); ?></a>
                                </p>
                            </div>
                            <?php else : ?>
                            <!-- Brand Voice (premium only) -->
                            <div class="tgai-field-group" id="tgai-brand-voice-section">
                                <label class="tgai-field-label" for="tgai_writing_style_id">
                                    <span class="dashicons dashicons-microphone" style="vertical-align: middle;"></span>
                                    <?php esc_html_e('Brand Voice', 'talkgenai'); ?>
                                </label>
                                <select id="tgai_writing_style_id" name="writing_style_id" class="regular-text" style="width:100%;max-width:400px;display:none;">
                                    <option value=""><?php esc_html_e('Default TalkGenAI style', 'talkgenai'); ?></option>
                                </select>
                                <p id="tgai-brand-voice-no-voices" class="tgai-free-hint" style="margin-top:4px;">
                                    <?php esc_html_e('No brand voices yet. ', 'talkgenai'); ?>
                                    <a href="https://app.talkgen.ai/dashboard" target="_blank"><?php esc_html_e('Create one on app.talkgen.ai →', 'talkgenai'); ?></a>
                                </p>
                                <p class="tgai-free-hint" id="tgai-brand-voice-hint" style="margin-top:4px;display:none;">
                                    <?php esc_html_e('Apply a brand voice you\'ve learned from your site. Manage voices on ', 'talkgenai'); ?>
                                    <a href="https://app.talkgen.ai/dashboard" target="_blank"><?php esc_html_e('app.talkgen.ai', 'talkgenai'); ?></a>.
                                </p>
                            </div>
                            <?php endif; ?>

                            <!-- Generate Article Button -->
                            <button type="submit" class="button button-primary tgai-generate-btn" id="generate-article-btn">
                                <span class="dashicons dashicons-edit"></span>
                                <?php echo $is_free ? esc_html__('Generate Short Article', 'talkgenai') : esc_html__('Generate Article', 'talkgenai'); ?>
                            </button>

                        </form>
                        
                        <!-- Article Result Area -->
                        <div id="article-result-area" style="display: none; margin-top: 30px;">
                            <div style="display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 15px;">
                                <h3 style="margin: 0; padding-top: 5px;"><?php esc_html_e('Generated Article', 'talkgenai'); ?></h3>
                                <div class="talkgenai-article-header-right">
                                    <div class="talkgenai-article-actions-top">
                                        <!-- Primary Action: Create Draft (revealed after generation) -->
                                        <span id="create-draft-group" style="display:none;">
                                            <span class="talkgenai-create-draft-group">
                                                <span class="talkgenai-post-type-toggle">
                                                    <button type="button" class="talkgenai-toggle-btn active" data-value="post"><?php esc_html_e('Post', 'talkgenai'); ?></button>
                                                    <button type="button" class="talkgenai-toggle-btn" data-value="page"><?php esc_html_e('Page', 'talkgenai'); ?></button>
                                                </span>
                                                <input type="hidden" id="draft-post-type" value="post">
                                                <button type="button" class="button" id="create-draft-btn">
                                                    <span class="dashicons dashicons-welcome-write-blog" style="vertical-align: middle; margin-right: 3px;"></span><span class="talkgenai-draft-btn-label"><?php esc_html_e('Create Post Draft', 'talkgenai'); ?></span>
                                                </button>
                                            </span>
                                        </span>

                                        <!-- Secondary: More options dropdown -->
                                        <div class="talkgenai-more-options-wrapper">
                                            <button type="button" class="button talkgenai-more-options-btn" id="more-options-btn" aria-expanded="false">
                                                <span class="dashicons dashicons-admin-tools" style="vertical-align: middle; margin-right: 2px;"></span><?php esc_html_e('More', 'talkgenai'); ?><span class="talkgenai-more-chevron">&#9662;</span>
                                            </button>
                                            <div class="talkgenai-more-options-dropdown" id="more-options-dropdown">
                                                <button type="button" class="talkgenai-dropdown-item" id="copy-visual-btn-top">
                                                    <span class="dashicons dashicons-clipboard"></span><?php esc_html_e('Copy Visual', 'talkgenai'); ?>
                                                </button>
                                                <button type="button" class="talkgenai-dropdown-item" id="copy-code-btn-top">
                                                    <span class="dashicons dashicons-editor-code"></span><?php esc_html_e('Copy Code', 'talkgenai'); ?>
                                                </button>
                                                <button type="button" class="talkgenai-dropdown-item" id="copy-meta-btn-top" style="display:none;">
                                                    <span class="dashicons dashicons-tag"></span><?php esc_html_e('Copy Meta', 'talkgenai'); ?>
                                                </button>
                                                <button type="button" class="talkgenai-dropdown-item" id="download-article-btn-top">
                                                    <span class="dashicons dashicons-download"></span><?php esc_html_e('Download HTML', 'talkgenai'); ?>
                                                </button>
                                            </div>
                                        </div>
                                    </div>

                                    <!-- FAQ Schema Badge (shown when FAQ schema passes validation) -->
                                    <div id="faq-schema-badge" class="talkgenai-faq-badge" style="display:none;">
                                        <span class="dashicons dashicons-yes-alt"></span>
                                        <?php esc_html_e('FAQ Schema validated — eligible for Google rich snippets', 'talkgenai'); ?>
                                        <a href="https://developers.google.com/search/docs/appearance/structured-data/faqpage" target="_blank" class="talkgenai-faq-badge-link"><?php esc_html_e('Learn more', 'talkgenai'); ?></a>
                                    </div>
                                </div>
                            </div>

                            <!-- Tab Navigation -->
                            <div class="talkgenai-tabs">
                                <button type="button" class="talkgenai-tab-button active" data-tab="visual"><?php esc_html_e('Visual', 'talkgenai'); ?></button>
                                <button type="button" class="talkgenai-tab-button" data-tab="code"><?php esc_html_e('Code', 'talkgenai'); ?></button>
                            </div>
                            
                            <!-- Tab Content -->
                            <div class="talkgenai-tab-content">
                                <!-- Visual Tab -->
                                <div id="visual-tab" class="talkgenai-tab-panel active">
                                    <div id="article-content" class="article-visual-content"></div>
                                </div>
                                
                                <!-- Code Tab -->
                                <div id="code-tab" class="talkgenai-tab-panel">
                                    <div id="article-code" class="article-code-content"></div>
                                </div>
                            </div>
                            
                            <!-- Action Buttons -->
                            <div class="talkgenai-article-actions">
                                <button type="button" class="button" id="copy-visual-btn"><?php esc_html_e('Copy Visual', 'talkgenai'); ?></button>
                                <button type="button" class="button" id="copy-code-btn"><?php esc_html_e('Copy Code', 'talkgenai'); ?></button>
                                <button type="button" class="button" id="download-article-btn"><?php esc_html_e('Download HTML', 'talkgenai'); ?></button>
                            </div>
                        </div>
                        
                        <!-- Meta Description Section (Outside article result area) -->
                        <div class="talkgenai-meta-description-section" style="display: none; margin-top: 30px; padding: 20px; background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px;">
                            <h3 style="margin-top: 0; color: #333;"><?php esc_html_e('Meta Description', 'talkgenai'); ?></h3>
                            <p class="description"><?php esc_html_e('SEO-optimized meta description (155-160 characters) for search engines:', 'talkgenai'); ?></p>
                            <div class="talkgenai-meta-description-container" style="position: relative;">
                                <textarea id="meta-description-text" readonly style="width: 100%; height: 60px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; font-size: 14px; resize: vertical; background: #fff;"></textarea>
                                <button type="button" class="button button-secondary" id="copy-meta-description-btn" style="position: absolute; top: 5px; right: 5px;">
                                    <span class="dashicons dashicons-clipboard"></span> <?php esc_html_e('Copy', 'talkgenai'); ?>
                                </button>
                            </div>
                            <div class="talkgenai-meta-description-info" style="margin-top: 10px; font-size: 12px; color: #666;">
                                <span id="meta-description-length">0</span> <?php esc_html_e('characters', 'talkgenai'); ?>
                            </div>
                        </div>
                    </div>
                </div>
                
                <!-- Sidebar -->
                <div class="talkgenai-sidebar">
                    <!-- How It Works -->
                    <div class="talkgenai-sidebar-box">
                        <h3><?php esc_html_e('How It Works', 'talkgenai'); ?></h3>
                        <ol style="margin: 0; padding-left: 20px;">
                            <li><?php esc_html_e('Choose App-Based or Standalone', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Enter title & topic (or select an app)', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Configure SEO options & links', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Click Generate Article', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Copy or download the result', 'talkgenai'); ?></li>
                        </ol>
                    </div>

                    <!-- Article Features -->
                    <div class="talkgenai-sidebar-box">
                        <h3><?php esc_html_e('Article Features', 'talkgenai'); ?></h3>
                        <p style="margin: 0;"><?php esc_html_e('Your article will include:', 'talkgenai'); ?></p>
                        <ul style="margin: 10px 0 0 20px;">
                            <li><?php esc_html_e('SEO-optimized structure (H2, H3)', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Internal links to your content', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('External authority link', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('FAQ section with schema markup', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Meta description for SEO', 'talkgenai'); ?></li>
                            <li><?php esc_html_e('Professional, engaging content', 'talkgenai'); ?></li>
                        </ul>
                    </div>

                    <!-- Article History Section -->
                    <div class="talkgenai-sidebar-box">
                        <h3><?php esc_html_e('Article History', 'talkgenai'); ?> <span id="article-history-count" style="font-size: 14px; color: #666;">(0)</span></h3>
                        <p style="margin: 0 0 10px 0; font-size: 13px;"><?php esc_html_e('Load or delete previously generated articles:', 'talkgenai'); ?></p>

                        <select id="article-history-select" class="regular-text" style="width: 100%; margin-bottom: 10px;">
                            <option value="">-- Select from history --</option>
                        </select>

                        <div style="display: flex; gap: 5px;">
                            <button type="button" id="load-article-from-history-btn" class="button button-secondary" style="flex: 1;">
                                <span class="dashicons dashicons-download" style="vertical-align: middle;"></span> <?php esc_html_e('Load', 'talkgenai'); ?>
                            </button>
                            <button type="button" id="delete-article-from-history-btn" class="button button-secondary" style="flex: 1;">
                                <span class="dashicons dashicons-trash" style="vertical-align: middle;"></span> <?php esc_html_e('Delete', 'talkgenai'); ?>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            <?php endif; // End has_api_key check ?>
        </div>

        <?php if ($has_api_key) : ?>
        <!-- Article Form UI Script -->
        <script type="text/javascript">
        jQuery(document).ready(function($) {
            var $toggle = $('#tgai-source-toggle');

            // --- Pill Toggle: update slider position ---
            function updatePillSlider() {
                var val = $('input[name="article_source"]:checked').val();
                if (val === 'app-based') {
                    $toggle.addClass('tgai-pill--right');
                } else {
                    $toggle.removeClass('tgai-pill--right');
                }
                $toggle.find('.tgai-pill-toggle__option').each(function() {
                    var $opt = $(this);
                    if ($opt.find('input').is(':checked')) {
                        $opt.addClass('tgai-pill-toggle__option--active');
                    } else {
                        $opt.removeClass('tgai-pill-toggle__option--active');
                    }
                });
            }
            updatePillSlider();

            // Toggle app-based fields + animate
            $('input[name="article_source"]').on('change', function() {
                updatePillSlider();
                var isAppBased = ($(this).val() === 'app-based');
                if (isAppBased) {
                    $('.talkgenai-app-based-field').slideDown(250);
                } else {
                    $('.talkgenai-app-based-field').slideUp(200);
                }
            });

            // --- Length Pills ---
            $('.tgai-length-pill').on('click', function() {
                if ($(this).hasClass('tgai-premium-locked')) return;
                var val = $(this).data('value');
                $('#article_length').val(val);
                $('.tgai-length-pill').removeClass('active');
                $(this).addClass('active');
            });

            // --- Collapsible Section ---
            $('.tgai-collapsible__header').on('click', function() {
                var $section = $(this).closest('.tgai-collapsible');
                var $body = $section.find('.tgai-collapsible__body');
                if ($section.hasClass('collapsed')) {
                    $section.removeClass('collapsed');
                    $body.slideDown(250);
                } else {
                    $section.addClass('collapsed');
                    $body.slideUp(200);
                }
            });

            // --- Auto-populate title & topic when an app is selected ---
            $('#target_app').on('change', function() {
                var appId = $(this).val();
                if (!appId) return;

                var nonce = (typeof talkgenai_nonce !== 'undefined') ? talkgenai_nonce : (talkgenai_ajax && talkgenai_ajax.nonce);
                $.ajax({
                    url: ajaxurl,
                    type: 'POST',
                    dataType: 'json',
                    data: {
                        action: 'talkgenai_load_app',
                        nonce: nonce,
                        app_id: appId,
                        id: appId
                    },
                    success: function(resp) {
                        var data = resp && (resp.data || resp.app || resp);
                        var meta = data && (data.app || data.meta || data);
                        var title = (meta && (meta.title || meta.name || meta.app_title)) || '';
                        var desc = (meta && (meta.description || meta.app_description || meta.summary)) || '';

                        if ((!title || !desc) && data && data.json_spec && data.json_spec.page) {
                            var page = data.json_spec.page;
                            if (!title && (page.title || page.name)) {
                                title = (page.title || page.name || '').toString();
                            }
                            if (!desc && (page.description || page.summary)) {
                                desc = (page.description || page.summary || '').toString();
                            }
                        }

                        if (!title) {
                            title = $('#target_app option:selected').text() || '';
                        }

                        var $titleField = $('#article_title');
                        var $topicField = $('#article_topic');

                        if (!$titleField.val() || $titleField.data('auto-populated')) {
                            $titleField.val(title).data('auto-populated', true);
                        }
                        if (!$topicField.val() || $topicField.data('auto-populated')) {
                            $topicField.val(desc).data('auto-populated', true);
                        }
                    }
                });
            });

            // Mark fields as user-edited when they type
            $('#article_title').on('input', function() { $(this).data('auto-populated', false); });
            $('#article_topic').on('input', function() { $(this).data('auto-populated', false); });

            // --- Brand Voice dropdown (premium only) ---
            (function loadBrandVoices() {
                var nonce = (typeof talkgenai_nonce !== 'undefined') ? talkgenai_nonce : (talkgenai_ajax && talkgenai_ajax.nonce);
                $.ajax({
                    url: ajaxurl,
                    type: 'POST',
                    dataType: 'json',
                    data: { action: 'talkgenai_get_writing_styles', nonce: nonce },
                    success: function(resp) {
                        if (!resp || !resp.success) return;
                        var styles = (resp.data && resp.data.styles) ? resp.data.styles : [];
                        var activeId = (resp.data && resp.data.active_id) ? resp.data.active_id : '';
                        var $select = $('#tgai_writing_style_id');
                        if (styles.length === 0) {
                            // Keep the "no voices" message visible
                            return;
                        }
                        styles.forEach(function(s) {
                            var $opt = $('<option>').val(s.id).text(s.name);
                            if (s.id === activeId) { $opt.prop('selected', true); }
                            $select.append($opt);
                        });
                        // Hide "no voices" message, show dropdown + hint
                        $('#tgai-brand-voice-no-voices').hide();
                        $select.show();
                        $('#tgai-brand-voice-hint').show();
                    }
                });
            })();
        });
        </script>
        <?php endif; // End has_api_key check for JS block ?>
        <?php
    }

    /**
     * Render settings page
     * 
     * Note: Nonce verification not required - this is a display-only function.
     * Form submission is handled by WordPress Settings API via options.php
     * which includes built-in nonce verification via settings_fields().
     * The $settings variable contains data from database, not user input.
     * 
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    public function render_settings_page() {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        
        if (!current_user_can(TALKGENAI_ADMIN_CAPABILITY)) {
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'talkgenai'));
        }
        
        $settings = $this->api->get_settings();
        $is_debug_mode = defined('WP_DEBUG') && WP_DEBUG;
        ?>
        <div class="wrap">
            <h1><?php esc_html_e('TalkGenAI Settings', 'talkgenai'); ?></h1>
            
            <?php if ($is_debug_mode): ?>
                <div class="notice notice-info">
                    <p>
                        <strong><?php esc_html_e('Debug Mode Enabled', 'talkgenai'); ?></strong> - 
                        <?php esc_html_e('Advanced developer settings are visible. These settings are hidden for regular users.', 'talkgenai'); ?>
                    </p>
                </div>
            <?php endif; ?>
            
            <form method="post" action="options.php">
                <?php
                settings_fields('talkgenai_settings_group');
                do_settings_sections('talkgenai_settings');
                ?>
                
                <table class="form-table">
                    <?php if ($is_debug_mode): ?>
                    <tr>
                        <th scope="row"><?php esc_html_e('Server Mode', 'talkgenai'); ?></th>
                        <td>
                            <fieldset>
                                <label>
                                    <input type="radio" name="talkgenai_settings[server_mode]" value="local" 
                                           <?php checked($settings['server_mode'], 'local'); ?>>
                                    <?php esc_html_e('Local Server', 'talkgenai'); ?>
                                </label><br>
                                <label>
                                    <input type="radio" name="talkgenai_settings[server_mode]" value="remote" 
                                           <?php checked($settings['server_mode'], 'remote'); ?>>
                                    <?php esc_html_e('Remote Server', 'talkgenai'); ?>
                                </label>
                            </fieldset>
                            <p class="description">
                                <?php esc_html_e('Choose whether to connect to a local development server or remote production server.', 'talkgenai'); ?>
                                <br><em><?php esc_html_e('(Developer setting - hidden in production)', 'talkgenai'); ?></em>
                            </p>
                        </td>
                    </tr>
                    
                    <tr>
                        <th scope="row">
                            <label for="local_server_url"><?php esc_html_e('Local Server URL', 'talkgenai'); ?></label>
                        </th>
                        <td>
                            <input type="url" id="local_server_url" name="talkgenai_settings[local_server_url]" 
                                   value="<?php echo esc_attr($settings['local_server_url']); ?>" class="regular-text">
                            <p class="description">
                                <?php esc_html_e('URL of your local TalkGenAI server (e.g., http://localhost:8000)', 'talkgenai'); ?>
                                <br><em><?php esc_html_e('(Developer setting - hidden in production)', 'talkgenai'); ?></em>
                            </p>
                        </td>
                    </tr>
                    
                    <tr>
                        <th scope="row">
                            <label for="remote_server_url"><?php esc_html_e('Remote Server URL', 'talkgenai'); ?></label>
                        </th>
                        <td>
                            <input type="url" id="remote_server_url" name="talkgenai_settings[remote_server_url]" 
                                   value="<?php echo esc_attr($settings['remote_server_url']); ?>" class="regular-text">
                            <p class="description">
                                <?php esc_html_e('URL of your remote TalkGenAI server', 'talkgenai'); ?>
                                <br><em><?php esc_html_e('(Developer setting - hidden in production)', 'talkgenai'); ?></em>
                            </p>
                        </td>
                    </tr>
                    <?php else: ?>
                    <!-- Hidden inputs for settings when not in debug mode -->
                    <?php
                    // Nonce verification handled by WordPress Settings API (settings_fields on line 1247)
                    // phpcs:ignore WordPress.Security.NonceVerification.Missing
                    ?>
                    <input type="hidden" name="talkgenai_settings[server_mode]" value="remote">
                    <input type="hidden" name="talkgenai_settings[remote_server_url]" value="https://app.talkgen.ai">
                    <?php endif; ?>
                    
                    <tr>
                        <th scope="row">
                            <label for="remote_api_key"><?php esc_html_e('Remote API Key', 'talkgenai'); ?></label>
                        </th>
                        <td>
                            <?php if (empty($settings['remote_api_key'])): ?>
                            <div class="notice notice-info inline" style="margin: 0 0 15px 0; padding: 10px;">
                                <p style="margin: 0;">
                                    <strong><?php esc_html_e('Getting Started:', 'talkgenai'); ?></strong><br>
                                    <?php 
                                    printf(
                                        /* translators: %1$s: opening link tag, %2$s: closing link tag */
                                        esc_html__('Don\'t have an API key yet? %1$sSign up for free at TalkGenAI%2$s to create your account and generate your API key. The free tier includes 10 generation credits to get started!', 'talkgenai'),
                                        '<a href="https://app.talkgen.ai/" target="_blank" rel="noopener"><strong>',
                                        '</strong></a>'
                                    );
                                    ?>
                                </p>
                            </div>
                            <?php endif; ?>
                            
                            <input type="password" id="remote_api_key" name="talkgenai_settings[remote_api_key]" 
                                   value="<?php echo esc_attr($settings['remote_api_key']); ?>" class="regular-text">
                            <p class="description">
                                <?php esc_html_e('Enter your TalkGenAI API key to connect to the service.', 'talkgenai'); ?>
                            </p>
                        </td>
                    </tr>
                    
                    <?php if ($is_debug_mode): ?>
                    <tr>
                        <th scope="row">
                            <label for="max_requests_per_hour"><?php esc_html_e('Max Requests Per Hour', 'talkgenai'); ?></label>
                        </th>
                        <td>
                            <input type="number" id="max_requests_per_hour" name="talkgenai_settings[max_requests_per_hour]" 
                                   value="<?php echo esc_attr($settings['max_requests_per_hour']); ?>" 
                                   min="1" max="1000" class="small-text">
                            <p class="description">
                                <?php esc_html_e('Maximum number of API requests per user per hour', 'talkgenai'); ?>
                                <br><em><?php esc_html_e('(Developer setting - hidden in production)', 'talkgenai'); ?></em>
                            </p>
                        </td>
                    </tr>
                    
                    <tr>
                        <th scope="row"><?php esc_html_e('Debug Logging', 'talkgenai'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="talkgenai_settings[enable_debug_logging]" value="1" 
                                       <?php checked($settings['enable_debug_logging']); ?>>
                                <?php esc_html_e('Enable debug logging', 'talkgenai'); ?>
                            </label>
                            <p class="description">
                                <?php esc_html_e('Enable detailed logging for troubleshooting (only when WP_DEBUG is also enabled)', 'talkgenai'); ?>
                                <br><em><?php esc_html_e('(Developer setting - hidden in production)', 'talkgenai'); ?></em>
                            </p>
                        </td>
                    </tr>
                    <?php endif; ?>
                </table>
                
                <div class="talkgenai-settings-actions">
                    <?php submit_button(__('Save Settings', 'talkgenai'), 'primary', 'submit', false); ?>
                    <button type="button" class="button" id="test-connection-btn">
                        <?php esc_html_e('Test Connection', 'talkgenai'); ?>
                    </button>
                    <?php if ($is_debug_mode): ?>
                    <button type="button" class="button" id="auto-detect-btn">
                        <?php esc_html_e('Auto-Detect Local Server', 'talkgenai'); ?>
                    </button>
                    <?php endif; ?>
                </div>
                
                <div id="connection-test-result" style="margin-top: 20px;"></div>
            </form>
        </div>
        <?php
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
    }
    
    /**
     * Handle AJAX app generation
     */
    public function handle_generate_app() {
        // Increase timeout for app generation (2 minutes)
        // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Necessary for long-running AI generation operations
        set_time_limit(120);
        
        // Security checks
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class method; inputs sanitized via security class sanitize_user_input()
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability();
        
        // Rate limiting (enhanced with detailed error handling)
        $rate_limit_result = $this->security->check_rate_limit(get_current_user_id(), 'generate');
        if (is_wp_error($rate_limit_result)) {
            wp_send_json_error(array(
                'message' => $rate_limit_result->get_error_message(),
                'code' => $rate_limit_result->get_error_code()
            ));
        } elseif (!$rate_limit_result) {
            wp_send_json_error(array(
                'message' => __('Rate limit exceeded. Please try again later.', 'talkgenai')
            ));
        }
        
        // Get and validate input
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Inputs sanitized via security class sanitize_user_input()
        $description = isset($_POST['description']) ? $this->security->sanitize_user_input(wp_unslash($_POST['description']), 'textarea') : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Input sanitized via security class sanitize_user_input()
        $title = $this->security->sanitize_user_input(wp_unslash($_POST['title'] ?? ''), 'text');
        
        if (empty($description)) {
            wp_send_json_error(array(
                'message' => __('Description is required.', 'talkgenai')
            ));
        }
        
        // Generate title if not provided
        if (empty($title)) {
            $title = talkgenai_generate_title($description);
        }
        
        // Call API
        $result = $this->api->generate_app($description);
        
        if (is_wp_error($result)) {
            // Check if this is an insufficient credits error
            if ($result->get_error_code() === 'insufficient_credits') {
                $error_data = $result->get_error_data();
                $upgrade_url = isset($error_data['upgrade_url']) ? $error_data['upgrade_url'] : 'https://app.talkgen.ai/';
                $credits_remaining = isset($error_data['credits_remaining']) ? $error_data['credits_remaining'] : 0;
                $plan = isset($error_data['plan']) ? $error_data['plan'] : 'free';
                
                // Return structured error with HTML message and upgrade link
                wp_send_json_error(array(
                    'message' => $result->get_error_message(),
                    'error_code' => 'insufficient_credits',
                    'ai_message' => '<div class="talkgenai-error-notice" style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; padding: 16px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107; max-width: 500px;">
                        <div style="display: flex; align-items: center; margin-bottom: 12px;">
                            <span style="font-size: 22px; margin-right: 8px;">⚠️</span>
                            <strong style="color: #856404; font-size: 1.1em;">Out of Credits</strong>
                        </div>
                        <div style="color: #856404; margin-bottom: 12px;">
                            <p style="margin: 0 0 8px 0;">' . esc_html($result->get_error_message()) . '</p>
                            <p style="margin: 0; font-size: 0.9em;">Credits Remaining: <strong>' . esc_html($credits_remaining) . '</strong></p>
                            <p style="margin: 0; font-size: 0.9em;">Current Plan: <strong>' . esc_html(ucfirst($plan)) . '</strong></p>
                        </div>
                        <div style="text-align: center; margin-top: 16px;">
                            <a href="' . esc_url($upgrade_url) . '" target="_blank" style="display: inline-block; padding: 12px 24px; background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: white; text-decoration: none; border-radius: 6px; font-weight: 600; transition: transform 0.2s;">
                                Upgrade to Starter Plan →
                            </a>
                        </div>
                    </div>',
                    'is_html' => true
                ));
            } else {
                wp_send_json_error(array(
                    'message' => $result->get_error_message()
                ));
            }
        }
        
        // Safety check and fix for json_spec
        if (!isset($result['json_spec']) || empty($result['json_spec'])) {
            
            // Try to find json_spec in other possible fields
            $possible_fields = ['specification', 'spec', 'json_specification', 'app_spec'];
            foreach ($possible_fields as $field) {
                if (isset($result[$field]) && !empty($result[$field])) {
                    $result['json_spec'] = $result[$field];
                    break;
                }
            }
            
            // If still no json_spec, create a minimal one
            if (!isset($result['json_spec']) || empty($result['json_spec'])) {
                $result['json_spec'] = array(
                    'appClass' => 'calculator',
                    'appType' => 'calculator_form',
                    'title' => $title,
                    'description' => $description
                );
            }
        }
        
        // Use CSS/JS from server response (server already separated them)
        // Fall back to extraction only if server didn't provide them
        if (isset($result['css']) && isset($result['js'])) {
            // Server provided separate CSS/JS fields - use them directly
            $content_parts = array(
                'html' => $result['html'],
                'css' => $result['css'],
                'js' => $result['js']
            );
        } else {
            // Fallback: extract from HTML (legacy behavior)
            $content_parts = $this->separate_html_css_and_js($result['html']);
        }
        
        // Sanitize HTML content (without JS or CSS)
        $sanitized_html = $this->security->sanitize_html_content($content_parts['html']);
        
        // Prepare data for saving (will be separated again in database->save_app)
        $app_data = array(
            'user_id' => get_current_user_id(),
            'title' => $title,
            'description' => $description,
            'app_class' => $result['json_spec']['appClass'],
            'app_type' => $result['json_spec']['appType'],
            'html_content' => $result['html'], // Original content for saving
            'json_spec' => wp_json_encode($result['json_spec']), // For new apps, use the complete spec as-is
            'generation_time' => $result['generation_time'] ?? null,
            'server_response_time' => $result['server_response_time'] ?? null
        );
        
        wp_send_json_success(array(
            'html' => $sanitized_html, // HTML only for preview
            'css' => $content_parts['css'], // CSS for preview
            'js' => $content_parts['js'], // JavaScript for preview
            'json_spec' => $result['json_spec'],
            'ai_message' => isset($result['ai_message']) ? $result['ai_message'] : null,
            'app_data' => $app_data,
            'generation_time' => $result['generation_time'] ?? null
        ));
    }
    
    /**
     * Handle AJAX app modification
     */
    public function handle_modify_app() {
        // Security checks
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class method
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability();
        
        // Rate limiting
        if (!$this->security->check_rate_limit(get_current_user_id(), 'modify')) {
            wp_send_json_error(array(
                'message' => __('Rate limit exceeded. Please try again later.', 'talkgenai')
            ));
        }
        
        // Get and validate input
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified above
        $app_id = intval($_POST['app_id'] ?? 0);
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Input sanitized via security class sanitize_user_input()
        $modification = $this->security->sanitize_user_input(wp_unslash($_POST['modification']), 'textarea');
        
        // Validate required fields
        if (empty($app_id)) {
            wp_send_json_error(array(
                'message' => __('App ID is required.', 'talkgenai')
            ));
        }
        
        if (empty($modification)) {
            wp_send_json_error(array(
                'message' => __('Modification description is required.', 'talkgenai')
            ));
        }
        
        // Load current spec from database (Single Source of Truth)
        $app = $this->database->get_app($app_id);
        
        if (!$app || is_wp_error($app)) {
            wp_send_json_error(array(
                'message' => __('App not found or access denied.', 'talkgenai')
            ));
        }
        
        // Get and validate json_spec from database
        $json_spec_string = $app['json_spec'] ?? '';
        if (empty($json_spec_string)) {
            wp_send_json_error(array(
                'message' => __('App specification not found in database.', 'talkgenai')
            ));
        }
        
        // Decode and sanitize the spec (WordPress.org requirement)
        $current_spec_raw = json_decode($json_spec_string, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($current_spec_raw)) {
            wp_send_json_error(array(
                'message' => __('Invalid JSON specification in database: ', 'talkgenai') . json_last_error_msg()
            ));
        }
        // Sanitize decoded JSON (WordPress.org requirement)
        $current_spec = $this->sanitize_decoded_json($current_spec_raw);
        
        // Validate required fields in spec
        if (!isset($current_spec['appClass']) || !isset($current_spec['appType'])) {
            wp_send_json_error(array(
                'message' => __('App specification missing required fields (appClass, appType).', 'talkgenai')
            ));
        }
        
        // Apply Unicode decoding to the spec before sending to Python server (if needed)
        $current_spec_for_api = method_exists($this, 'decode_unicode_in_spec') 
            ? $this->decode_unicode_in_spec($current_spec)
            : $current_spec;
        
        // Call API with modification description and current spec from database
        $result = $this->api->modify_app($modification, $current_spec_for_api);
        
        if (is_wp_error($result)) {
            // Check if this is an insufficient credits error
            if ($result->get_error_code() === 'insufficient_credits') {
                $error_data = $result->get_error_data();
                $upgrade_url = isset($error_data['upgrade_url']) ? $error_data['upgrade_url'] : 'https://app.talkgen.ai/';
                $credits_remaining = isset($error_data['credits_remaining']) ? $error_data['credits_remaining'] : 0;
                $plan = isset($error_data['plan']) ? $error_data['plan'] : 'free';
                
                // Return structured error with HTML message and upgrade link
                wp_send_json_error(array(
                    'message' => $result->get_error_message(),
                    'error_code' => 'insufficient_credits',
                    'ai_message' => '<div class="talkgenai-error-notice" style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; padding: 16px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107; max-width: 500px;">
                        <div style="display: flex; align-items: center; margin-bottom: 12px;">
                            <span style="font-size: 22px; margin-right: 8px;">⚠️</span>
                            <strong style="color: #856404; font-size: 1.1em;">Out of Credits</strong>
                        </div>
                        <div style="color: #856404; margin-bottom: 12px;">
                            <p style="margin: 0 0 8px 0;">' . esc_html($result->get_error_message()) . '</p>
                            <p style="margin: 0; font-size: 0.9em;">Credits Remaining: <strong>' . esc_html($credits_remaining) . '</strong></p>
                            <p style="margin: 0; font-size: 0.9em;">Current Plan: <strong>' . esc_html(ucfirst($plan)) . '</strong></p>
                        </div>
                        <div style="text-align: center; margin-top: 16px;">
                            <a href="' . esc_url($upgrade_url) . '" target="_blank" style="display: inline-block; padding: 12px 24px; background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: white; text-decoration: none; border-radius: 6px; font-weight: 600; transition: transform 0.2s;">
                                Upgrade to Starter Plan →
                            </a>
                        </div>
                    </div>',
                    'is_html' => true
                ));
            } else {
                wp_send_json_error(array(
                    'message' => $result->get_error_message()
                ));
            }
        }
        
        // CRITICAL FIX: Handle both delta responses and complete specs with changes
        // Check if json_spec is incomplete (contains only styling delta instead of full spec)
        $json_spec_is_incomplete = false;
        $is_delta_response = false;
        if (isset($result['json_spec']) && isset($current_spec_decoded)) {
            $spec = $result['json_spec'];
            // Check if this looks like a delta response (has stylingDelta but missing core app fields)
            $has_styling_delta = isset($spec['stylingDelta']);
            $has_ui_text_delta = isset($spec['uiTextDelta']);
            $missing_core_fields = !isset($spec['appClass']) || !isset($spec['form']) || !isset($spec['directCalculation']);
            $json_spec_is_incomplete = ($has_styling_delta || $has_ui_text_delta) && $missing_core_fields;
            $is_delta_response = $has_styling_delta || $has_ui_text_delta;
        }
        
        if ($json_spec_is_incomplete) {
            // This is a style-only modification - merge delta with current spec
            $styling_delta = $result['json_spec']['stylingDelta'] ?? null;
            $ui_text_delta = $result['json_spec']['uiTextDelta'] ?? null;
            
            $result['json_spec'] = $current_spec_decoded;
            
            // Apply styling delta if present
            if (isset($styling_delta['elementStyles'])) {
                if (!isset($result['json_spec']['styling'])) {
                    $result['json_spec']['styling'] = array();
                }
                if (!isset($result['json_spec']['styling']['customStyling'])) {
                    $result['json_spec']['styling']['customStyling'] = array();
                }
                if (!isset($result['json_spec']['styling']['customStyling']['elementStyles'])) {
                    $result['json_spec']['styling']['customStyling']['elementStyles'] = array();
                }
                
                // Merge the new element styles
                $result['json_spec']['styling']['customStyling']['elementStyles'] = array_merge(
                    $result['json_spec']['styling']['customStyling']['elementStyles'],
                    $styling_delta['elementStyles']
                );
            }
            
            // Apply resultCardStyles delta if present
            if (isset($styling_delta['resultCardStyles'])) {
                if (!isset($result['json_spec']['styling'])) {
                    $result['json_spec']['styling'] = array();
                }
                if (!isset($result['json_spec']['styling']['customStyling'])) {
                    $result['json_spec']['styling']['customStyling'] = array();
                }
                if (!isset($result['json_spec']['styling']['customStyling']['resultCardStyles'])) {
                    $result['json_spec']['styling']['customStyling']['resultCardStyles'] = array();
                }
                
                // Merge the new result card styles
                $result['json_spec']['styling']['customStyling']['resultCardStyles'] = array_merge(
                    $result['json_spec']['styling']['customStyling']['resultCardStyles'],
                    $styling_delta['resultCardStyles']
                );
            }
            
            // Apply itemColors delta if present
            if (isset($styling_delta['itemColors'])) {
                // Apply itemColors to resultsDisplay items
                if (!isset($result['json_spec']['resultsDisplay'])) {
                    $result['json_spec']['resultsDisplay'] = array();
                }
                if (!isset($result['json_spec']['resultsDisplay']['sections'])) {
                    $result['json_spec']['resultsDisplay']['sections'] = array();
                }
                
                foreach ($result['json_spec']['resultsDisplay']['sections'] as &$section) {
                    if (isset($section['items']) && is_array($section['items'])) {
                        foreach ($section['items'] as &$item) {
                            $value_key = isset($item['value']) ? $item['value'] : (isset($item['field']) ? $item['field'] : null);
                            if ($value_key && isset($styling_delta['itemColors'][$value_key])) {
                                $item['color'] = $styling_delta['itemColors'][$value_key];
                            }
                        }
                    }
                }
            }
            
            // Apply cardColors delta if present
            if (isset($styling_delta['cardColors']) && is_array($styling_delta['cardColors'])) {
                if (!isset($result['json_spec']['styling'])) {
                    $result['json_spec']['styling'] = array();
                }
                if (!isset($result['json_spec']['styling']['customStyling'])) {
                    $result['json_spec']['styling']['customStyling'] = array();
                }
                
                $result['json_spec']['styling']['customStyling']['cardColors'] = $styling_delta['cardColors'];
            }
            
            // Apply uiText delta if present
            if (isset($ui_text_delta['labels'])) {
                if (!isset($result['json_spec']['uiText'])) {
                    $result['json_spec']['uiText'] = array();
                }
                if (!isset($result['json_spec']['uiText']['labels'])) {
                    $result['json_spec']['uiText']['labels'] = array();
                }
                
                // Merge the new labels
                $result['json_spec']['uiText']['labels'] = array_merge(
                    $result['json_spec']['uiText']['labels'],
                    $ui_text_delta['labels']
                );
            }
        }
        
        // Handle complete specs that may have changes (not delta responses)
        if (!$json_spec_is_incomplete && isset($result['json_spec']) && isset($current_spec_decoded)) {
            // Check if this is a complete spec with potential changes
            $spec = $result['json_spec'];
            if (isset($spec['appClass']) && isset($spec['form']) && isset($spec['directCalculation'])) {
                // This is a complete spec - use it directly
                $result['json_spec'] = $spec;
            }
        }
        
        // Use CSS/JS from server response (server already separated them)
        // Fall back to extraction only if server didn't provide them
        if (isset($result['css']) && isset($result['js'])) {
            // Server provided separate CSS/JS fields - use them directly
            $content_parts = array(
                'html' => $result['html'],
                'css' => $result['css'],
                'js' => $result['js']
            );
        } else {
            // Fallback: extract from HTML (legacy behavior)
            $content_parts = $this->separate_html_css_and_js($result['html']);
        }
        
        // Sanitize HTML content (without CSS/JS)
        $sanitized_html = $this->security->sanitize_html_content($content_parts['html']);
        
        wp_send_json_success(array(
            'html' => $sanitized_html, // HTML only for preview
            'css' => $content_parts['css'], // CSS for preview
            'js' => $content_parts['js'], // JavaScript for preview
            'json_spec' => $result['json_spec'], // This is now guaranteed to be complete thanks to our fix above
            'ai_message' => isset($result['ai_message']) ? $result['ai_message'] : null,
            'generation_time' => $result['generation_time'] ?? null
        ));
    }
    
    /**
     * Handle AJAX app deletion
     */
    public function handle_delete_app() {
        // error_log('TalkGenAI DELETE HANDLER: handle_delete_app() called');
        
        // Security checks
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class method
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability();
        
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified above
        $app_id = intval($_POST['app_id'] ?? 0);
        // error_log('TalkGenAI DELETE HANDLER: App ID received: ' . $app_id);
        
        if (!$app_id) {
            // error_log('TalkGenAI DELETE HANDLER: Invalid app ID, aborting');
            wp_send_json_error(array(
                'message' => __('Invalid app ID.', 'talkgenai')
            ));
        }
        
        // Delete static files before deleting database record
        // error_log('TalkGenAI DELETE HANDLER: About to call delete_static_files() for app ' . $app_id);
        // error_log('TalkGenAI DELETE HANDLER: file_generator object: ' . (isset($this->file_generator) ? 'EXISTS' : 'NULL'));
        
        if (isset($this->file_generator)) {
            $delete_result = $this->file_generator->delete_static_files($app_id);
            // error_log('TalkGenAI DELETE HANDLER: delete_static_files() returned: ' . ($delete_result ? 'TRUE' : 'FALSE'));
        } else {
            // error_log('TalkGenAI DELETE HANDLER ERROR: file_generator is not initialized!');
        }
        
        // error_log('TalkGenAI DELETE HANDLER: About to delete from database');
        $result = $this->database->delete_app($app_id);
        
        if (is_wp_error($result)) {
            // error_log('TalkGenAI DELETE HANDLER ERROR: Database deletion failed: ' . $result->get_error_message());
            wp_send_json_error(array(
                'message' => $result->get_error_message()
            ));
        }
        
        // error_log('TalkGenAI DELETE HANDLER: Successfully deleted app ' . $app_id . ' from database');
        
        // NEW: Notify service to decrement active_apps counter
        $service_result = $this->api->delete_app($app_id);
        if (isset($service_result['success']) && $service_result['success']) {
            // error_log('TalkGenAI DELETE HANDLER: Service notified, counter decremented');
            // Clear stats cache so next request gets fresh data
            $this->api->clear_stats_cache();
        } else {
            // error_log('TalkGenAI DELETE HANDLER WARNING: Failed to notify service: ' . ($service_result['error'] ?? 'Unknown error'));
            // Don't fail the delete if service notification fails
        }
        
        wp_send_json_success(array(
            'message' => __('App deleted successfully.', 'talkgenai'),
            'stats' => $service_result['stats'] ?? null
        ));
    }
    
    /**
     * Handle AJAX connection test
     */
    public function handle_test_connection() {
        // Security checks
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class method
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability(TALKGENAI_ADMIN_CAPABILITY);
        
        $result = $this->api->test_connection(true); // Force fresh check
        
        wp_send_json_success($result);
    }

    /**
     * Handle AJAX connection test for debugging save issues
     */
    public function handle_debug_save_test() {
        // error_log('TalkGenAI DEBUG: Debug save test called');
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Debug endpoint for diagnostics, no sensitive operations
        wp_send_json_success(array(
            'message' => 'Debug endpoint reached successfully',
            'post_max_size' => ini_get('post_max_size'),
            'max_input_vars' => ini_get('max_input_vars'),
            'content_length' => isset($_SERVER['CONTENT_LENGTH']) ? sanitize_text_field(wp_unslash($_SERVER['CONTENT_LENGTH'])) : 'not set',
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Debug endpoint for diagnostics only, no sensitive data processing
            'post_keys_count' => count($_POST)
        ));
    }

    /**
     * Handle save chunk for chunked uploads
     * CRITICAL: Read from php://input to bypass WordPress magic quotes and get raw UTF-8
     */
    public function handle_save_chunk() {
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.Security.NonceVerification.Missing -- Debug code wrapped in WP_DEBUG check; nonce verified below from JSON input
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNK: handle_save_chunk() called');
        
        // CRITICAL FIX: Read JSON from php://input to bypass WordPress processing
        $raw_input = file_get_contents('php://input');
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI CHUNK: Raw input length: ' . strlen($raw_input));
            // error_log('TalkGenAI CHUNK: Content-Type: ' . (isset($_SERVER['CONTENT_TYPE']) ? sanitize_text_field(wp_unslash($_SERVER['CONTENT_TYPE'])) : 'not set'));
        }
        
        // Parse and sanitize JSON input (WordPress.org requirement: sanitize after json_decode)
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately below
        $input_raw = json_decode($raw_input, true);
        
        // Validate and sanitize decoded data (WordPress.org requirement)
        if (!is_array($input_raw)) {
            $input = array();
        } else {
            $input = $this->sanitize_decoded_json($input_raw);
        }
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            // error_log('TalkGenAI CHUNK ERROR: Failed to parse JSON input: ' . json_last_error_msg());
            wp_send_json_error(array('message' => 'Invalid JSON input: ' . json_last_error_msg()));
            return;
        }
        
        // Verify nonce from JSON input (sanitize since json_decode doesn't sanitize)
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce from JSON input, sanitized here
        if (!isset($input['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($input['nonce'])), 'talkgenai_nonce')) {
            // error_log('TalkGenAI CHUNK ERROR: Nonce verification failed');
            wp_send_json_error(array('message' => 'Security check failed'), 403);
            return;
        }
        
        // Extract data from JSON payload (sanitize all fields individually)
        $session_id = sanitize_text_field($input['session_id'] ?? '');
        $chunk_index = intval($input['chunk_index'] ?? 0);
        $total_chunks = intval($input['total_chunks'] ?? 1);
        $field_name = sanitize_text_field($input['field_name'] ?? '');
        $chunk_data = $input['chunk_data'] ?? ''; // RAW UTF-8 data - DO NOT SANITIZE!
        
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug code wrapped in WP_DEBUG check
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI CHUNK: Session {$session_id}, chunk {$chunk_index}/{$total_chunks}, field {$field_name}");
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI CHUNK: Chunk data length: " . strlen($chunk_data));
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI CHUNK: Title in JSON: " . (isset($input['title']) ? 'YES (' . $input['title'] . ')' : 'NO'));
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI CHUNK: Description in JSON: " . (isset($input['description']) ? 'YES (' . substr($input['description'], 0, 50) . '...)' : 'NO'));
 
        // Extra diagnostics for json_spec chunks
        if ($field_name === 'json_spec' && defined('WP_DEBUG') && WP_DEBUG) {
            $len = is_string($chunk_data) ? strlen($chunk_data) : 0;
            $prefix = is_string($chunk_data) ? substr($chunk_data, 0, 100) : '';
            $looks_base64 = is_string($chunk_data) && (bool)preg_match('/^[A-Za-z0-9+\/\n\r]*={0,2}$/', $chunk_data);
            // error_log("TalkGenAI CHUNK DEBUG: json_spec chunk {$chunk_index}/{$total_chunks} length={$len}, base64_format=" . ($looks_base64 ? 'YES' : 'NO'));
            // error_log("TalkGenAI CHUNK DEBUG: json_spec chunk prefix (100): " . $prefix);
            // Try strict decode probe
            $probe = base64_decode($chunk_data, true);
            if ($probe === false) {
                // error_log("TalkGenAI CHUNK DEBUG: base64 strict decode: FAILED for chunk {$chunk_index}");
            } else {
                // error_log("TalkGenAI CHUNK DEBUG: base64 strict decode: OK, decoded_prefix: " . substr($probe, 0, 100));
            }
        }
        
        if (empty($session_id)) {
            wp_send_json_error(array('message' => 'Missing session ID'));
            return;
        }
        
        // Store chunk in WordPress transients (auto-expires after 1 hour)
        // CRITICAL: chunk_data is ALREADY base64-encoded by JavaScript (admin.js line 1933)
        // DO NOT double-encode or it will fail to decode properly!
        $chunk_key = "talkgenai_chunk_{$session_id}_{$chunk_index}";
        $chunk_info = array(
            'session_id' => $session_id,
            'chunk_index' => $chunk_index,
            'total_chunks' => $total_chunks,
            'field_name' => $field_name,
            'chunk_data' => $chunk_data, // Already base64-encoded from JavaScript
            'timestamp' => time(),
            'other_data' => $input // Store other JSON data with each chunk
        );
        
        // Remove chunk-specific keys from other_data to avoid duplication
        unset($chunk_info['other_data']['session_id']);
        unset($chunk_info['other_data']['chunk_index']);
        unset($chunk_info['other_data']['total_chunks']);
        unset($chunk_info['other_data']['field_name']);
        unset($chunk_info['other_data']['chunk_data']);
        unset($chunk_info['other_data']['action']);
        unset($chunk_info['other_data']['nonce']);
        
        $stored = set_transient($chunk_key, $chunk_info, 3600); // 1 hour expiry
        
        if ($stored) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug code wrapped in WP_DEBUG check
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI CHUNK: Successfully stored chunk {$chunk_index}");
            wp_send_json_success(array(
                'message' => "Chunk {$chunk_index} saved successfully",
                'chunk_index' => $chunk_index,
                'session_id' => $session_id
            ));
        } else {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug code wrapped in WP_DEBUG check
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI CHUNK: Failed to store chunk {$chunk_index}");
            wp_send_json_error(array('message' => 'Failed to store chunk'));
        }
    }

    /**
     * Handle finalize chunked save - reassemble chunks and save app
     * CRITICAL: Read from php://input to bypass WordPress magic quotes
     */
    public function handle_finalize_chunked_save() {
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.Security.NonceVerification.Missing -- Debug code wrapped in WP_DEBUG check; nonce verified below from JSON input
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: handle_finalize_chunked_save() called');
        
        // CRITICAL FIX: Read JSON from php://input
        $raw_input = file_get_contents('php://input');
        // Parse and sanitize JSON input (WordPress.org requirement: sanitize after json_decode)
        $input_raw = json_decode($raw_input, true);
        
        // Validate and sanitize decoded data (WordPress.org requirement)
        if (!is_array($input_raw)) {
            $input = array();
        } else {
            $input = $this->sanitize_decoded_json($input_raw);
        }
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            // error_log('TalkGenAI FINALIZE ERROR: Failed to parse JSON: ' . json_last_error_msg());
            wp_send_json_error(array('message' => 'Invalid JSON input: ' . json_last_error_msg()));
            return;
        }
        
        // Verify nonce from JSON input (sanitize since json_decode doesn't sanitize)
        if (!isset($input['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($input['nonce'])), 'talkgenai_nonce')) {
            // error_log('TalkGenAI FINALIZE ERROR: Nonce verification failed');
            wp_send_json_error(array('message' => 'Security check failed'), 403);
            return;
        }
        
        // Extract and sanitize data from JSON payload
        $session_id = sanitize_text_field($input['session_id'] ?? '');
        
        if (empty($session_id)) {
            wp_send_json_error(array('message' => 'Missing session ID'));
            return;
        }
        
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI FINALIZE: Processing session {$session_id}");
        
        // Get all chunks for this session
        $chunks = $this->get_chunks_for_session($session_id);
        
        if (empty($chunks)) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI FINALIZE: No chunks found for session {$session_id}");
            wp_send_json_error(array('message' => 'No chunks found for session'));
            return;
        }
        
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI FINALIZE: Found " . count($chunks) . " chunks");
        
        // Reassemble the data
        $reassembled_data = $this->reassemble_chunks($chunks);
        
        if (!$reassembled_data) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI FINALIZE: Failed to reassemble chunks");
            wp_send_json_error(array('message' => 'Failed to reassemble chunks'));
            return;
        }
        
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("TalkGenAI FINALIZE: Successfully reassembled data");
        if (defined('WP_DEBUG') && WP_DEBUG && is_array($reassembled_data)) {
            // error_log('TalkGenAI FINALIZE: Reassembled keys: ' . implode(', ', array_keys($reassembled_data)));
        }
        
        // Clean up chunks
        $this->cleanup_chunks($session_id);
        
        // Sanitize and merge reassembled data (WordPress.org requirement: never assign to $_POST directly)
        // Instead, sanitize individual fields as we extract them
        $sanitized_data = array();
        
        // Sanitize app_class
        if (isset($reassembled_data['app_class'])) {
            $sanitized_data['app_class'] = sanitize_text_field($reassembled_data['app_class']);
        } elseif (isset($reassembled_data['ac'])) {
            $sanitized_data['app_class'] = sanitize_text_field($reassembled_data['ac']);
        } else {
            $sanitized_data['app_class'] = 'calculator'; // Safe default
        }
        
        // Sanitize app_type
        if (isset($reassembled_data['app_type'])) {
            $sanitized_data['app_type'] = sanitize_text_field($reassembled_data['app_type']);
        } elseif (isset($reassembled_data['at'])) {
            $sanitized_data['app_type'] = sanitize_text_field($reassembled_data['at']);
        } else {
            $sanitized_data['app_type'] = 'productivity'; // Safe default
        }
        
        // Sanitize other fields
        if (isset($reassembled_data['title'])) {
            $sanitized_data['title'] = sanitize_text_field($reassembled_data['title']);
        }
        if (isset($reassembled_data['description'])) {
            $sanitized_data['description'] = sanitize_textarea_field($reassembled_data['description']);
        }
        // HTML content: support both legacy ('html') and new ('html_content') keys from reassembly
        if (isset($reassembled_data['html_content']) || isset($reassembled_data['html'])) {
            $raw_html = isset($reassembled_data['html_content']) ? $reassembled_data['html_content'] : $reassembled_data['html'];
            $sanitized_data['html_content'] = $raw_html; // Will be sanitized in save handler
            $sanitized_data['html'] = $raw_html; // Also store with short name for save handler
        }

        // CSS content: support both 'css_content' and 'css'
        if (isset($reassembled_data['css_content']) || isset($reassembled_data['css'])) {
            $raw_css = isset($reassembled_data['css_content']) ? $reassembled_data['css_content'] : $reassembled_data['css'];
            $sanitized_data['css_content'] = $raw_css; // Will be sanitized in save handler
            $sanitized_data['css'] = $raw_css; // Also store with short name for save handler
        }

        // JS content: support both 'js_content' and 'js'
        if (isset($reassembled_data['js_content']) || isset($reassembled_data['js'])) {
            $raw_js = isset($reassembled_data['js_content']) ? $reassembled_data['js_content'] : $reassembled_data['js'];
            $sanitized_data['js_content'] = $raw_js; // Will be sanitized in save handler
            $sanitized_data['js'] = $raw_js; // Also store with short name for save handler
        }
        if (isset($reassembled_data['json_spec'])) {
            $sanitized_data['json_spec'] = $reassembled_data['json_spec']; // JSON string, will be validated
        }
        if (isset($reassembled_data['app_id'])) {
            $sanitized_data['app_id'] = absint($reassembled_data['app_id']);
        }
        
        // Merge sanitized data into $_POST
        $_POST = array_merge($_POST, $sanitized_data);
        $_POST['_is_chunked_save'] = true;
        
        // Debug logging removed for WordPress.org compliance (no unsanitized $_POST in logs)

        // Recover app_id if missing using HTTP referer (edit screen URL)
        if (empty($_POST['app_id'])) {
            $referer = isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
            if (is_string($referer) && $referer !== '') {
                $query = wp_parse_url($referer, PHP_URL_QUERY);
                if (is_string($query) && $query !== '') {
                    parse_str($query, $qs);
                    if (!empty($qs['app_id'])) {
                        $_POST['app_id'] = intval($qs['app_id']);
                        // error_log('TalkGenAI FINALIZE: Recovered app_id from referer: ' . $_POST['app_id']);
                    }
                }
            }
        }

        // Decide target handler STRICTLY by presence of app_id (ignore incoming action like talkgenai_finalize_chunk)
        $app_id_int = intval($_POST['app_id'] ?? 0);
        $decided_action = ($app_id_int > 0) ? 'talkgenai_update_app' : 'talkgenai_save_app';

        // Ensure title and description are present - prioritize json_spec extraction since remote server strips FormData fields
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Data came from reassembled chunks, already processed
        $title = $_POST['title'] ?? '';
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Data came from reassembled chunks, already processed
        $description = $_POST['description'] ?? '';
        
        // Always try to extract from json_spec first since it's more reliable on remote servers
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Data from reassembled chunks, extracted fields sanitized before storage
        $raw_spec = isset($_POST['json_spec']) ? wp_unslash($_POST['json_spec']) : '';
        if (is_string($raw_spec) && $raw_spec !== '') {
            // Note: json_decode does not sanitize - extracted fields are sanitized individually
            $decoded = json_decode($raw_spec, true);
            
            // Validate decoded data is an array (WordPress security requirement)
            if (is_array($decoded)) {
                // Extract title from json_spec if missing or if we want to prioritize json_spec
                // Values extracted here are sanitized by $this->security->sanitize_user_input() below before storage
                if (empty($title) && !empty($decoded['page']['title'])) {
            $title = $decoded['page']['title'];
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: Extracted title from json_spec: ' . $title);
                } elseif (empty($title) && !empty($decoded['uiText']['labels']['calculatorTitle'])) {
            $title = $decoded['uiText']['labels']['calculatorTitle'];
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: Extracted title from calculatorTitle: ' . $title);
                }
                
                // Extract description from json_spec if missing
                if (empty($description) && !empty($decoded['page']['description'])) {
            $description = $decoded['page']['description'];
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: Extracted description from json_spec: ' . substr($description, 0, 50) . '...');
                } elseif (empty($description) && !empty($decoded['uiText']['labels']['calculatorTitle'])) {
            $description = $decoded['uiText']['labels']['calculatorTitle'] . ' calculator';
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: Derived description from calculatorTitle: ' . $description);
                }
            }
        }
        
        // Fallback defaults if still empty
        if (empty($title)) {
            $title = 'Generated App';
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: Using fallback title: ' . $title);
        }
        if (empty($description)) {
            $description = 'AI Generated Application';
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI FINALIZE: Using fallback description: ' . $description);
        }
        
        // Set the extracted/derived values (sanitize before assignment)
        $_POST['title'] = sanitize_text_field($title);
        $_POST['description'] = sanitize_textarea_field($description);
        
        // Debug logging removed for WordPress.org compliance (no unsanitized $_POST in logs)
        
        // Call the appropriate save handler directly without security re-check
        // since chunks were already validated during upload
        if ($decided_action === 'talkgenai_update_app') {
            // error_log("TalkGenAI FINALIZE: Calling update handler (bypassing security re-check)");
            $this->handle_update_app_chunked();
        } else {
            // error_log("TalkGenAI FINALIZE: Calling save handler (bypassing security re-check)");
            $this->handle_save_app_chunked();
        }
    }

    /**
     * Get all chunks for a session
     */
    private function get_chunks_for_session($session_id) {
        global $wpdb;
        
        $chunks = array();
        
        // Robust retrieval: scan a safe upper bound of indices without relying on per-field total_chunks
        $MAX_SCAN = 200; // hard cap
        for ($i = 0; $i < $MAX_SCAN; $i++) {
            $chunk_key = "talkgenai_chunk_{$session_id}_{$i}";
            $chunk = get_transient($chunk_key);
            if ($chunk !== false) {
                $chunks[$i] = $chunk;
            }
        }
        
        // Sort by index to keep stable order
        ksort($chunks);
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI FINALIZE DEBUG: Retrieved ' . count($chunks) . ' chunks for session ' . $session_id);
        }
        
        return $chunks;
    }

    /**
     * Reassemble chunks into original data
     */
    private function reassemble_chunks($chunks) {
        if (empty($chunks)) {
            return false;
        }
        
        // Merge base data from ALL chunks (not only the first) to avoid missing fields
        $reassembled_data = array();
        foreach ($chunks as $c) {
            if (!empty($c['other_data']) && is_array($c['other_data'])) {
                foreach ($c['other_data'] as $k => $v) {
                    // Prefer the first non-empty value we encounter for each key
                    if (!array_key_exists($k, $reassembled_data) || (empty($reassembled_data[$k]) && !empty($v))) {
                        $reassembled_data[$k] = $v;
                    }
                }
            }
        }
        
        // Group chunks by field name
        $fields = array();
        
        foreach ($chunks as $chunk) {
            $field_name = $chunk['field_name'] ?? '';
            $chunk_index = $chunk['chunk_index'] ?? 0;
            $chunk_data = $chunk['chunk_data'] ?? '';
            
            if (!empty($field_name)) {
                if (!isset($fields[$field_name])) {
                    $fields[$field_name] = array();
                }
                $fields[$field_name][$chunk_index] = $chunk_data;
            }
        }
        
        // Reassemble each field
        foreach ($fields as $field_name => $field_chunks) {
            // Sort chunks by index
            ksort($field_chunks);
            
            // CRITICAL FIX: Chunks are base64-encoded by JavaScript (admin.js) to prevent UTF-8 corruption
            // Decode each chunk before concatenating
            $decoded_chunks = array();
            foreach ($field_chunks as $idx => $chunk_data) {
                // Base64-decode the chunk (it was encoded by JavaScript before transmission)
                $decoded = base64_decode($chunk_data, true);
                if ($decoded === false) {
                    // Decoding failed - log error
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        // error_log("TalkGenAI REASSEMBLE ERROR: Failed to base64-decode chunk {$idx} for field {$field_name}");
                    }
                    $decoded = $chunk_data; // Use raw data as fallback
                }
                $decoded_chunks[$idx] = $decoded;
            }
            
            // Concatenate the decoded chunks
            $assembled = implode('', $decoded_chunks);
            
            // CRITICAL: Detect and fix double-encoding bug
            // If assembled data looks like base64, it means it was double-encoded
            if ($field_name === 'json_spec' || $field_name === 'html' || $field_name === 'css' || $field_name === 'js') {
                $looks_like_base64 = preg_match('/^[A-Za-z0-9+\/\n\r]+={0,2}$/', trim($assembled));
                $first_char = substr(trim($assembled), 0, 1);
                
                // JSON should start with '{' or '[', HTML with '<', CSS/JS with various chars
                // If it looks like base64 instead, decode it again!
                if ($looks_like_base64 && $first_char !== '{' && $first_char !== '[' && $first_char !== '<') {
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        // error_log("TalkGenAI REASSEMBLE WARNING: Field '{$field_name}' looks double-encoded (base64)! Decoding again...");
                    }
                    
                    $double_decoded = base64_decode($assembled, true);
                    if ($double_decoded !== false) {
                        $assembled = $double_decoded;
                        if (defined('WP_DEBUG') && WP_DEBUG) {
                            // error_log("TalkGenAI REASSEMBLE: Double-encoding fixed! New first char: '" . substr(trim($assembled), 0, 1) . "'");
                        }
                    }
                }
            }
            
            // Debug: Show preview for json_spec
            if ($field_name === 'json_spec' && defined('WP_DEBUG') && WP_DEBUG) {
                // error_log("TalkGenAI REASSEMBLE DEBUG: json_spec assembled from " . count($field_chunks) . " chunks (base64-decoded)");
                // error_log("TalkGenAI REASSEMBLE DEBUG: json_spec preview (first 500 chars): " . substr($assembled, 0, 500));
                // error_log("TalkGenAI REASSEMBLE DEBUG: json_spec tail (last 500 chars): " . substr($assembled, -500));
                // error_log("TalkGenAI REASSEMBLE DEBUG: Total length: " . strlen($assembled) . " bytes");
                
                // Check for common issues
                $first_char = substr(trim($assembled), 0, 1);
                $last_char = substr(trim($assembled), -1, 1);
                // error_log("TalkGenAI REASSEMBLE DEBUG: First char: '{$first_char}', Last char: '{$last_char}'");
                
                // Check for backslashes (indicates escaping issue)
                $backslash_count = substr_count($assembled, '\\"');
                if ($backslash_count > 0) {
                    // error_log("TalkGenAI REASSEMBLE DEBUG: WARNING - Found {$backslash_count} escaped quotes (\\\") - data still has backslashes!");
                }
                
                // Check if it looks like JSON or JavaScript
                if (strpos($assembled, 'function(') !== false || strpos($assembled, '=>') !== false) {
                    // error_log("TalkGenAI REASSEMBLE DEBUG: WARNING - Contains JavaScript syntax, not pure JSON!");
                }
                
                // Test json_decode immediately to catch encoding issues
                $test_decode = json_decode($assembled, true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    // error_log("TalkGenAI REASSEMBLE DEBUG: json_decode test FAILED: " . json_last_error_msg());
                    // error_log("TalkGenAI REASSEMBLE DEBUG: Encoding check: " . mb_detect_encoding($assembled, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true));
                    
                    // Find approximately where the error is
                    for ($i = 100; $i < strlen($assembled); $i += 100) {
                        $partial = substr($assembled, 0, $i);
                        json_decode($partial, true);
                        if (json_last_error() !== JSON_ERROR_NONE && json_last_error() !== JSON_ERROR_STATE_MISMATCH) {
                            // error_log("TalkGenAI REASSEMBLE DEBUG: Error appears around byte position {$i}");
                            // error_log("TalkGenAI REASSEMBLE DEBUG: Context: " . substr($assembled, max(0, $i - 50), 100));
                            break;
                        }
                    }
                } else {
                    // error_log("TalkGenAI REASSEMBLE DEBUG: json_decode test PASSED ✓");
                }
            }
            
            $reassembled_data[$field_name] = $assembled;
            
            // Debug reassembled field sizes
            // error_log("TalkGenAI REASSEMBLE: Field '{$field_name}' reassembled to " . strlen($reassembled_data[$field_name]) . " bytes from " . count($field_chunks) . " chunks (base64 decoded)");
        }
        
        // error_log('TalkGenAI REASSEMBLE: Final reassembled data keys: ' . implode(', ', array_keys($reassembled_data)));
        // error_log('TalkGenAI REASSEMBLE: Reassembled data summary:');
        foreach ($reassembled_data as $key => $value) {
            if (is_string($value)) {
                // error_log("  - {$key}: " . strlen($value) . " bytes");
            } else {
                // error_log("  - {$key}: " . gettype($value) . " (" . print_r($value, true) . ")");
            }
        }
        
        return $reassembled_data;
    }

    /**
     * Clean up chunks for a session
     */
    private function cleanup_chunks($session_id) {
        // Try to delete up to 50 possible chunks
        for ($i = 0; $i < 50; $i++) {
            $chunk_key = "talkgenai_chunk_{$session_id}_{$i}";
            delete_transient($chunk_key);
        }
        
        // error_log("TalkGenAI FINALIZE: Cleaned up chunks for session {$session_id}");
    }

    /**
     * Handle AJAX app save (creates a new app record)
     */
    public function handle_save_app() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class verify_ajax_nonce(); inputs sanitized via security class sanitize_user_input() or wp_unslash()
        // EARLY DEBUG: Log that we reached the handler
        // Debug logging removed for WordPress.org compliance (no unsanitized $_POST in logs)
        
        // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class verify_ajax_nonce(); inputs sanitized via security class sanitize_user_input() or wp_unslash()
        
        // Security checks
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability();

        // Validate input
        $title = $this->security->sanitize_user_input($_POST['title'] ?? '', 'text');
        $description = $this->security->sanitize_user_input($_POST['description'] ?? '', 'textarea');
        
        // Check unfiltered_html capability (admins on single-site, super-admins on multisite)
        $can_unfiltered_html = current_user_can('unfiltered_html');
        
        $html_content = is_string($_POST['html'] ?? null)
            ? ($can_unfiltered_html ? wp_unslash($_POST['html']) : wp_kses_post(wp_unslash($_POST['html'])))
            : ($_POST['html'] ?? '');
        
        $css_content = is_string($_POST['css'] ?? null)
            ? ($can_unfiltered_html ? wp_unslash($_POST['css']) : '') // Only allow CSS for trusted users
            : ($_POST['css'] ?? '');
            
        $js_content = is_string($_POST['js'] ?? null)
            ? ($can_unfiltered_html ? wp_unslash($_POST['js']) : '') // Only allow JS for trusted users
            : ($_POST['js'] ?? '');
        // Get json_spec - normalize_json_spec() will handle wp_unslash internally
        $json_spec = $_POST['json_spec'] ?? '';
        $app_class = sanitize_text_field($_POST['app_class'] ?? '');
        $app_type = sanitize_text_field($_POST['app_type'] ?? '');
        
        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

        // Debug logging
        // error_log('TalkGenAI SAVE: Starting save process');
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI SAVE: Received from client:');
            // error_log('  - html length: ' . strlen($html_content));
            // error_log('  - css length: ' . strlen($css_content));
            // error_log('  - js length: ' . strlen($js_content));
            // error_log('  - html has <style>: ' . (strpos($html_content, '<style>') !== false ? 'YES' : 'NO'));
            // error_log('  - css empty: ' . (empty($css_content) ? 'YES' : 'NO'));
        }
        // error_log('TalkGenAI Debug: Server POST limits - post_max_size: ' . ini_get('post_max_size') . ', max_input_vars: ' . ini_get('max_input_vars'));
        // error_log('TalkGenAI Debug: Request size: ' . (isset($_SERVER['CONTENT_LENGTH']) ? sanitize_text_field(wp_unslash($_SERVER['CONTENT_LENGTH'])) : 'not set') . ' bytes');
        // Debug logging removed for WordPress.org compliance
        // error_log('TalkGenAI Debug: js_content size: ' . strlen($js_content) . ' bytes');

        if (empty($title)) {
            $title = talkgenai_generate_title($description ?: '');
        }

        if (empty($description) || empty($html_content) || empty($json_spec) || empty($app_class) || empty($app_type)) {
            wp_send_json_error(array('message' => __('Missing required data to save app.', 'talkgenai')));
        }

        // Store HTML and JS separately (WordPress requirement: no inline scripts in HTML)
        // JavaScript will be enqueued separately using wp_add_inline_script() during rendering
        $combined_html = $html_content;
        $final_js = $js_content;

        // Prepare data for saving
        $app_data = array(
            'user_id' => get_current_user_id(),
            'title' => $title,
            'description' => $description,
            'app_class' => $app_class,
            'app_type' => $app_type,
            'html_content' => $combined_html,
            'css_content' => $css_content,  // Include CSS content
            'js_content' => $final_js,
            'json_spec' => $this->handle_json_spec_for_save($json_spec), // Handle escaped JSON properly
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
            'generation_time' => isset($_POST['generation_time']) ? floatval($_POST['generation_time']) : null,
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
            'server_response_time' => isset($_POST['server_response_time']) ? floatval($_POST['server_response_time']) : null
        );

        // Deny-list validation before saving: block only hard-fail patterns; warn otherwise
        if (!empty($final_js) && $this->security && method_exists($this->security, 'analyze_js_deny_list')) {
            // error_log('TalkGenAI Debug: Running JS security analysis on ' . strlen($final_js) . ' bytes of JS');
            $analysis = $this->security->analyze_js_deny_list($final_js);
            // error_log('TalkGenAI Debug: Security analysis result: ' . print_r($analysis, true));
            if (!empty($analysis['blocked'])) {
                // error_log('TalkGenAI ERROR: Blocked patterns: ' . print_r($analysis['blocked'], true));
                wp_send_json_error(array(
                    'message' => __('Unsafe JavaScript patterns detected: ', 'talkgenai') . implode(', ', $analysis['blocked'])
                ));
            }
            if (!empty($analysis['warnings']) && defined('WP_DEBUG') && WP_DEBUG) {
                // error_log('TalkGenAI: JS warnings on save: ' . implode(', ', $analysis['warnings']));
            }
        }

		$app_id = $this->database->insert_app($app_data);

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

        // Generate static CSS and JS files
        $file_results = $this->file_generator->generate_static_files($app_id, $app_data);
        
        // Update database with file URLs
        if (!is_wp_error($file_results) && ($file_results['css_generated'] || $file_results['js_generated'])) {
            $file_data = array();
            if ($file_results['css_generated']) {
                $file_data['css_file_url'] = $file_results['css_url'];
            }
            if ($file_results['js_generated']) {
                $file_data['js_file_url'] = $file_results['js_url'];
            }
            $file_data['file_version'] = $file_results['version'];
            
            $this->database->update_app($app_id, $file_data);
            
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // error_log('TalkGenAI: Generated static files for app ' . $app_id);
            }
        }

        // Notify backend about app save (increments active_apps counter for new apps)
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
        $existing_app_id = intval($_POST['existing_app_id'] ?? 0);
        $is_modification = ($existing_app_id > 0);
        $this->api->notify_app_saved($app_id, $is_modification);

        $shortcode = sprintf('[talkgenai_app id="%d"]', $app_id);

        wp_send_json_success(array(
            'app_id' => $app_id,
            'shortcode' => $shortcode
        ));
    }
    
    /**
     * Handle AJAX app save (chunked version without security re-check)
     */
    public function handle_save_app_chunked() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Security verified during chunk uploads; data from reassembled chunks
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI AJAX: handle_save_app_chunked() called (security already verified in chunks)');
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        // Debug logging removed for WordPress.org compliance (no unsanitized $_POST in logs)
        
        // Skip security checks - already verified during chunk uploads
        
        // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Security verified during chunk uploads; data from reassembled chunks
        
        // Validate input
        $title = $this->security->sanitize_user_input($_POST['title'] ?? '', 'text');
        $description = $this->security->sanitize_user_input($_POST['description'] ?? '', 'textarea');
        
        // CRITICAL: For chunked saves, data is NOT slashed by WordPress, so don't unslash!
        $is_chunked = isset($_POST['_is_chunked_save']) && $_POST['_is_chunked_save'] === true;
        
        $html_content = is_string($_POST['html'] ?? null)
            ? ($is_chunked ? $_POST['html'] : wp_unslash($_POST['html']))
            : ($_POST['html'] ?? '');
        $css_content = is_string($_POST['css'] ?? null)
            ? ($is_chunked ? $_POST['css'] : wp_unslash($_POST['css']))
            : ($_POST['css'] ?? '');
        $js_content = is_string($_POST['js'] ?? null)
            ? ($is_chunked ? $_POST['js'] : wp_unslash($_POST['js']))
            : ($_POST['js'] ?? '');
        $json_spec = is_string($_POST['json_spec'] ?? null)
            ? ($is_chunked ? $_POST['json_spec'] : wp_unslash($_POST['json_spec']))
            : ($_POST['json_spec'] ?? '');
        $app_class = sanitize_text_field($_POST['app_class'] ?? '');
        $app_type = sanitize_text_field($_POST['app_type'] ?? '');
        
        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

        // Log received data sizes for debugging
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI: Received data sizes - HTML: ' . strlen($html_content) . ', CSS: ' . strlen($css_content) . ', JS: ' . strlen($js_content) . ', JSON: ' . strlen($json_spec));

        // Validate required fields
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: Validating fields - title: ' . (empty($title) ? 'EMPTY' : 'OK(' . $title . ')'));
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: Validating fields - html_content: ' . (empty($html_content) ? 'EMPTY' : 'OK(' . strlen($html_content) . ' bytes)'));
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: Validating fields - js_content: ' . (empty($js_content) ? 'EMPTY' : 'OK(' . strlen($js_content) . ' bytes)'));
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: Validating fields - json_spec: ' . (empty($json_spec) ? 'EMPTY' : 'OK(' . strlen($json_spec) . ' bytes)'));
        
        if (empty($title)) {
            // error_log('TalkGenAI CHUNKED: FAILED validation - Title is empty');
            wp_send_json_error(array('message' => __('Title is required.', 'talkgenai')));
            return;
        }

        if (empty($html_content) || empty($js_content) || empty($json_spec)) {
           // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
           if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: FAILED validation - Missing app content');
           // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
           if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: html_content empty: ' . (empty($html_content) ? 'YES' : 'NO'));
           // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
           if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: js_content empty: ' . (empty($js_content) ? 'YES' : 'NO'));
           // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
           if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: json_spec empty: ' . (empty($json_spec) ? 'YES' : 'NO'));
            wp_send_json_error(array('message' => __('Missing required app content.', 'talkgenai')));
            return;
        }
        
           // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
           if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI CHUNKED: All required fields validated successfully');

        // CRITICAL: Clean JSON spec BEFORE trying to decode it
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: About to clean JSON spec (' . strlen($json_spec) . ' bytes)');
            // error_log('TalkGenAI DEBUG: Raw JSON spec preview: ' . substr($json_spec, 0, 200));
        }
        
        $json_spec = $this->handle_json_spec_for_save($json_spec);
        
        // Check if normalization failed
        if (is_wp_error($json_spec)) {
            wp_send_json_error(array(
                'message' => __('Invalid JSON specification: ', 'talkgenai') . $json_spec->get_error_message()
            ));
        }
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: JSON spec cleaned. New size: ' . strlen($json_spec) . ' bytes');
            // error_log('TalkGenAI DEBUG: Cleaned JSON preview: ' . substr($json_spec, 0, 200));
        }
        
        // Parse and validate JSON spec
        
        // Try to decode JSON normally first
        $decoded_spec = json_decode($json_spec, true);
        
        // Ensure proper UTF-8 encoding
        if (!mb_check_encoding($json_spec, 'UTF-8')) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // error_log('TalkGenAI DEBUG: Invalid UTF-8 encoding detected, attempting to fix');
            }
            $json_spec = mb_convert_encoding($json_spec, 'UTF-8', 'auto');
        }
        
        // Remove any remaining control characters that might interfere with JSON parsing
        $json_spec = preg_replace('/[\x00-\x1F\x7F]/', '', $json_spec);
        
        // Remove zero-width characters and other invisible Unicode characters
        $json_spec = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}\x{2060}]/u', '', $json_spec);
        
        // Remove any remaining non-printable characters except standard JSON characters
        $json_spec = preg_replace('/[^\x20-\x7E\x{0080}-\x{FFFF}]/u', '', $json_spec);
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: After UTF-8 fix and aggressive char removal, new length: ' . strlen($json_spec));
        }
        
        // More comprehensive character analysis around the error position
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: First 20 characters analysis:');
            for ($i = 0; $i < min(20, strlen($json_spec)); $i++) {
                $char = $json_spec[$i];
                $ord = ord($char);
                $hex = bin2hex($char);
                $visible = ($ord >= 32 && $ord <= 126) ? $char : '[NON-PRINTABLE]';
                // error_log("TalkGenAI DEBUG: pos $i: ord=$ord, hex=$hex, visible='$visible'");
            }
        }
        
        // Try to decode JSON with detailed error reporting
        try {
            $decoded_spec = json_decode($json_spec, true, 512, JSON_THROW_ON_ERROR);
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // error_log('TalkGenAI DEBUG: JSON decode successful with JSON_THROW_ON_ERROR');
            }
        } catch (JsonException $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // error_log('TalkGenAI DEBUG: JSON_THROW_ON_ERROR failed: ' . $e->getMessage());
                // error_log('TalkGenAI DEBUG: Error at position: ' . $e->getCode());
                
                // Scan for hidden/control characters around the error position
                $errorPos = $e->getCode();
                $start = max(0, $errorPos - 50);
                $end = min(strlen($json_spec), $errorPos + 50);
                $context = substr($json_spec, $start, $end - $start);
                
                // error_log('TalkGenAI DEBUG: Context around error (pos $errorPos): ' . $context);
                
                // Show exact character at error position
                if ($errorPos < strlen($json_spec)) {
                    $char = $json_spec[$errorPos];
                    $ord = ord($char);
                    // error_log('TalkGenAI DEBUG: Character at error position ' . $errorPos . ': ord=' . $ord . ', hex=' . bin2hex($char) . ', visible=' . ($ord >= 32 && $ord <= 126 ? $char : '[NON-PRINTABLE]'));
                }
                
                // Scan for non-printable characters
                $hiddenChars = [];
                for ($i = $start; $i < $end; $i++) {
                    $char = $json_spec[$i];
                    $ord = ord($char);
                    if ($ord < 32 || $ord > 126) {
                        if ($ord == 9 || $ord == 10 || $ord == 13) {
                            // Skip normal whitespace
                            continue;
                        }
                        $hiddenChars[] = "pos $i: char " . $ord . " (" . bin2hex($char) . ")";
                    }
                }
                
                if (!empty($hiddenChars)) {
                    // error_log('TalkGenAI DEBUG: Hidden characters found: ' . implode(", ", $hiddenChars));
                }
                
                // Try alternative JSON parsing methods
                // error_log('TalkGenAI DEBUG: Attempting alternative JSON parsing methods...');
                
                // Method 1: Try with different depth limit
                try {
                    $decoded_spec = json_decode($json_spec, true, 1024, JSON_THROW_ON_ERROR);
                    // error_log('TalkGenAI DEBUG: Alternative parsing (depth 1024) successful');
                } catch (JsonException $e2) {
                    // error_log('TalkGenAI DEBUG: Alternative parsing (depth 1024) failed: ' . $e2->getMessage());
                    
                    // Method 2: Try with very high depth limit
                    try {
                        $decoded_spec = json_decode($json_spec, true, 2048, JSON_THROW_ON_ERROR);
                        // error_log('TalkGenAI DEBUG: Alternative parsing (depth 2048) successful');
                    } catch (JsonException $e3) {
                        // error_log('TalkGenAI DEBUG: Alternative parsing (depth 2048) failed: ' . $e3->getMessage());
                        
                        // Method 3: Try with different flags
                        try {
                            $decoded_spec = json_decode($json_spec, true, 512, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE);
                            // error_log('TalkGenAI DEBUG: Alternative parsing (UTF8 ignore) successful');
                        } catch (JsonException $e4) {
                            // error_log('TalkGenAI DEBUG: Alternative parsing (UTF8 ignore) failed: ' . $e4->getMessage());
                            $decoded_spec = null;
                        }
                    }
                }
            }
            
            // Fall back to regular json_decode for compatibility
            if ($decoded_spec === null) {
                $decoded_spec = json_decode($json_spec, true);
            }
        }
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: json_decode completed. Error code: ' . json_last_error());
            if (json_last_error() !== JSON_ERROR_NONE) {
                // error_log('TalkGenAI DEBUG: JSON decode error message: ' . json_last_error_msg());
                // error_log('TalkGenAI DEBUG: First 500 chars of failed JSON: ' . substr($json_spec, 0, 500));
                // error_log('TalkGenAI DEBUG: Last 500 chars of failed JSON: ' . substr($json_spec, -500));
                // error_log('TalkGenAI DEBUG: JSON length: ' . strlen($json_spec));
                
                // Check if JSON is properly closed
                $openBraces = substr_count($json_spec, '{');
                $closeBraces = substr_count($json_spec, '}');
                $openBrackets = substr_count($json_spec, '[');
                $closeBrackets = substr_count($json_spec, ']');
                // error_log('TalkGenAI DEBUG: JSON structure check - Open braces: ' . $openBraces . ', Close braces: ' . $closeBraces . ', Open brackets: ' . $openBrackets . ', Close brackets: ' . $closeBrackets);
                
                // BYPASS PHP JSON PARSER - Use manual validation approach
                // error_log('TalkGenAI DEBUG: BYPASSING PHP json_decode - using manual validation');
                
                // Manual JSON validation and parsing
                $trimmed = trim($json_spec);
                if (strpos($trimmed, '{') === 0 && strrpos($trimmed, '}') === strlen($trimmed) - 1) {
                    // error_log('TalkGenAI DEBUG: Manual validation passed - looks like valid JSON');
                    
                    // Try a simple regex-based extraction for critical fields
                    $appClass = null;
                    $appType = null;
                    
                    if (preg_match('/"appClass"\s*:\s*"([^"]+)"/', $json_spec, $matches)) {
                        $appClass = $matches[1];
                    }
                    if (preg_match('/"appType"\s*:\s*"([^"]+)"/', $json_spec, $matches)) {
                        $appType = $matches[1];
                    }
                    
                    if ($appClass && $appType) {
                        // error_log('TalkGenAI DEBUG: Manual extraction successful - appClass: ' . $appClass . ', appType: ' . $appType);
                        
                        // Create a minimal valid JSON structure
                        $fallback_data = array(
                            'appClass' => $appClass,
                            'appType' => $appType,
                            'page' => array(
                                'title' => 'Manual Extraction',
                                'description' => 'Extracted via manual parsing due to PHP json_decode failure'
                            ),
                            'uiText' => array(),
                            'fields' => array(),
                            'styling' => array(),
                            'direction' => 'ltr',
                            'conversationalResponse' => 'App saved successfully via manual extraction',
                            'detectionReason' => 'Manual extraction due to PHP JSON parser failure',
                            'styleIntent' => 'default'
                        );
                        
                        // Convert to JSON string for database storage
                        $decoded_spec = wp_json_encode($fallback_data);
                        // error_log('TalkGenAI DEBUG: Created fallback JSON string with ' . count($fallback_data) . ' top-level keys, length: ' . strlen($decoded_spec));
                    } else {
                        // error_log('TalkGenAI DEBUG: Manual extraction failed - could not find appClass or appType');
                    }
                } else {
                    // error_log('TalkGenAI DEBUG: Manual validation failed - does not look like valid JSON');
                }
            } else {
                // error_log('TalkGenAI DEBUG: JSON decoded successfully');
            }
        }
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            wp_send_json_error(array('message' => __('Invalid JSON specification.', 'talkgenai')));
            return;
        }

        // Ensure final JS has security analysis
        $final_js = $js_content;
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: About to run JS security analysis');
        }
        
        if ($this->security && method_exists($this->security, 'analyze_js_deny_list')) {
               // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for security analysis
               if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI DEBUG: Running JS security analysis on ' . strlen($final_js) . ' bytes of JS');
               $analysis = $this->security->analyze_js_deny_list($final_js);
               // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Conditional debug logging for security analysis
               if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI DEBUG: Security analysis completed. Result: ' . print_r($analysis, true));
        } else {
            $analysis = array('is_safe' => true, 'violations' => array());
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for security analysis
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI DEBUG: Security analysis skipped (method not available)');
        }
        
        if (!$analysis['is_safe']) {
            wp_send_json_error(array(
                'message' => __('JavaScript contains disallowed patterns.', 'talkgenai'),
                'details' => $analysis['violations']
            ));
            return;
        }
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: Security check passed. About to prepare app_data array');
        }

        // Save to database via database layer to ensure security_hash is computed
        // NOTE: JSON spec was already cleaned earlier in the function
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: Preparing app_data with cleaned JSON spec (' . strlen($json_spec) . ' bytes)');
        }
        
        $app_data = array(
            'user_id' => get_current_user_id(),
            'title' => $title,
            'description' => $description,
            'app_class' => $app_class,
            'app_type' => $app_type,
            'html_content' => $html_content,
            'css_content' => $css_content,  // Include CSS content
            'js_content' => $final_js,
            'json_spec' => $json_spec,  // Use the already-cleaned JSON spec
        );
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: About to insert app into database');
        }

		$app_id = $this->database->insert_app($app_data);
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // error_log('TalkGenAI DEBUG: Database insert completed. App ID: ' . (is_wp_error($app_id) ? 'ERROR' : $app_id));
        }

        if (is_wp_error($app_id)) {
            // error_log('TalkGenAI: Database insert failed: ' . $app_id->get_error_message());
            wp_send_json_error(array('message' => __('Failed to save app to database.', 'talkgenai')));
            return;
        }

        // Generate static files after successful insert
        try {
            $app_data_from_db = $this->database->get_app($app_id);
            if ($app_data_from_db) {
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    // error_log('TalkGenAI CHUNKED: About to generate static files for app ' . $app_id);
                }
                
                $file_results = $this->file_generator->generate_static_files($app_id, $app_data_from_db);
                
                // Update database with new file URLs and version
                if (!is_wp_error($file_results) && ($file_results['css_generated'] || $file_results['js_generated'])) {
                    $file_data = array();
                    if ($file_results['css_generated']) {
                        $file_data['css_file_url'] = $file_results['css_url'];
                    }
                    if ($file_results['js_generated']) {
                        $file_data['js_file_url'] = $file_results['js_url'];
                    }
                    $file_data['file_version'] = $file_results['version'];
                    
                    $this->database->update_app($app_id, $file_data);
                    
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        // error_log('TalkGenAI CHUNKED: Generated static files for new app ' . $app_id);
                    }
                } else {
                    // error_log('TalkGenAI CHUNKED: File generation failed or returned WP_Error for app ' . $app_id);
                    if (is_wp_error($file_results)) {
                        // error_log('TalkGenAI CHUNKED: Error: ' . $file_results->get_error_message());
                    }
                }
            } else {
                // error_log('TalkGenAI CHUNKED: Could not retrieve app ' . $app_id . ' from database after insert');
            }
        } catch (Exception $e) {
            // error_log('TalkGenAI CHUNKED FATAL: Exception during file generation for app ' . $app_id . ': ' . $e->getMessage());
            // error_log('TalkGenAI CHUNKED FATAL: Stack trace: ' . $e->getTraceAsString());
        }

        // Notify backend about app save (increments active_apps counter for new apps)
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Security verified during chunk uploads
        $existing_app_id = intval($_POST['existing_app_id'] ?? 0);
        $is_modification = ($existing_app_id > 0);
        $this->api->notify_app_saved($app_id, $is_modification);

        // error_log("TalkGenAI: Successfully saved new app with ID: {$app_id}");

        wp_send_json_success(array(
            'message' => __('App saved successfully!', 'talkgenai'),
            'app_id' => $app_id,
            'redirect_url' => admin_url('admin.php?page=talkgenai&action=edit&app_id=' . $app_id)
        ));
    }
    
    /**
     * Handle AJAX app update (keeps same ID)
     */
    public function handle_update_app() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class verify_ajax_nonce(); inputs sanitized via security class sanitize_user_input() or wp_unslash()
        
        // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class verify_ajax_nonce(); inputs sanitized via security class sanitize_user_input() or wp_unslash()
        
        // Security checks
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability();
        
        $app_id = intval($_POST['app_id'] ?? 0);
        if (!$app_id) {
            wp_send_json_error(array('message' => __('Invalid app ID.', 'talkgenai')));
        }
        
        // Collect fields allowed to update
        $update = array();
        
        if (isset($_POST['title'])) {
            $update['title'] = $this->security->sanitize_user_input($_POST['title'], 'text');
        }
        if (isset($_POST['description'])) {
            $update['description'] = $this->security->sanitize_user_input($_POST['description'], 'textarea');
        }
        // Check unfiltered_html capability for code content
        $can_unfiltered_html = current_user_can('unfiltered_html');

        // Support both traditional and chunked keys for content fields
        $raw_html = isset($_POST['html']) ? $_POST['html'] : ( $_POST['html_content'] ?? null );
        if (isset($raw_html)) {
            $update['html_content'] = is_string($raw_html)
                ? ($can_unfiltered_html ? wp_unslash($raw_html) : wp_kses_post(wp_unslash($raw_html)))
                : '';
        }

        $raw_css = isset($_POST['css']) ? $_POST['css'] : ( $_POST['css_content'] ?? null );
        if (isset($raw_css)) {
            // For non-unfiltered users we currently store empty CSS (safety)
            $update['css_content'] = is_string($raw_css)
                ? ($can_unfiltered_html ? wp_unslash($raw_css) : '')
                : '';
        }

        $raw_js = isset($_POST['js']) ? $_POST['js'] : ( $_POST['js_content'] ?? null );
        if (isset($raw_js)) {
            // For non-unfiltered users we currently store empty JS (safety)
            $update['js_content'] = is_string($raw_js)
                ? ($can_unfiltered_html ? wp_unslash($raw_js) : '')
                : '';
        }
        if (isset($_POST['json_spec'])) {
            // DEBUG: Log what we're receiving
            $json_spec_raw = $_POST['json_spec'];
            // error_log('TalkGenAI UPDATE DEBUG: Received json_spec type: ' . gettype($json_spec_raw));
            // error_log('TalkGenAI UPDATE DEBUG: Received json_spec length: ' . (is_string($json_spec_raw) ? strlen($json_spec_raw) : 'not string'));
            // error_log('TalkGenAI UPDATE DEBUG: Received json_spec preview: ' . substr(print_r($json_spec_raw, true), 0, 500));
            
            if (is_string($json_spec_raw)) {
                // Handle potential double-escaping from WordPress/AJAX
                $clean_json = $json_spec_raw;
                
                // If the string starts with escaped quotes, it's been double-encoded
                if (strpos($clean_json, '\"') !== false) {
                    // error_log('TalkGenAI UPDATE DEBUG: Detected escaped JSON, cleaning...');
                    $clean_json = stripslashes($clean_json);
                }
                
                // Validate it's proper JSON and sanitize (WordPress.org requirement)
                $decoded_raw = json_decode($clean_json, true);
                
                // Sanitize decoded JSON data (WordPress.org requirement)
                if (!is_array($decoded_raw)) {
                    $decoded = array();
                } else {
                    $decoded = $this->sanitize_decoded_json($decoded_raw);
                }
                
                // error_log('TalkGenAI UPDATE DEBUG: JSON decode error: ' . json_last_error_msg());
                // error_log('TalkGenAI UPDATE DEBUG: Decoded is array: ' . (is_array($decoded) ? 'yes' : 'no'));
                
                if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                    // Re-encode with wp_json_encode() to ensure safe output
                    $update['json_spec'] = wp_json_encode($decoded);
                    // error_log('TalkGenAI UPDATE DEBUG: Using decoded and re-encoded JSON');
                } else {
                    // Only fall back to ensure_json_spec if the JSON is actually corrupted
                    $update['json_spec'] = $this->ensure_json_spec($json_spec_raw);
                    // error_log('TalkGenAI UPDATE DEBUG: Falling back to ensure_json_spec');
                }
            } else {
                $update['json_spec'] = wp_json_encode($json_spec_raw);
                // error_log('TalkGenAI UPDATE DEBUG: Direct encoding non-string input');
            }
            
            // error_log('TalkGenAI UPDATE DEBUG: Final json_spec for database: ' . substr($update['json_spec'], 0, 200));
        }
        
        if (empty($update)) {
            wp_send_json_error(array('message' => __('No fields to update.', 'talkgenai')));
        }
        
        $result = $this->database->update_app($app_id, $update);
        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_message()));
        }
        
        // Regenerate static files if content changed
        if (isset($update['css_content']) || isset($update['js_content']) || isset($update['html_content'])) {
            $app_data = $this->database->get_app($app_id);
            if ($app_data) {
                $file_results = $this->file_generator->generate_static_files($app_id, $app_data);
                
                // Update database with new file URLs and version
                if (!is_wp_error($file_results) && ($file_results['css_generated'] || $file_results['js_generated'])) {
                    $file_data = array();
                    if ($file_results['css_generated']) {
                        $file_data['css_file_url'] = $file_results['css_url'];
                    }
                    if ($file_results['js_generated']) {
                        $file_data['js_file_url'] = $file_results['js_url'];
                    }
                    $file_data['file_version'] = $file_results['version'];
                    
                    $this->database->update_app($app_id, $file_data);
                    
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        // error_log('TalkGenAI: Regenerated static files for app ' . $app_id);
                    }
                }
            }
        }
        
        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        
        $shortcode = sprintf('[talkgenai_app id="%d"]', $app_id);
        wp_send_json_success(array('app_id' => $app_id, 'shortcode' => $shortcode));
    }
    
    /**
     * Handle AJAX app update (chunked version without security re-check)
     */
    public function handle_update_app_chunked() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Security verified during chunk uploads; data from reassembled chunks
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging for chunked upload diagnostics
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('TalkGenAI AJAX: handle_update_app_chunked() called (security already verified in chunks)');
        
        // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Security verified during chunk uploads; data from reassembled chunks
        
        // Skip security checks - already verified during chunk uploads
        
        $app_id = intval($_POST['app_id'] ?? 0);
        if (!$app_id) {
            wp_send_json_error(array('message' => __('Invalid app ID.', 'talkgenai')));
        }
        
        // Collect fields allowed to update
        $update = array();
        
        if (isset($_POST['title'])) {
            $update['title'] = $this->security->sanitize_user_input($_POST['title'], 'text');
        }
        if (isset($_POST['description'])) {
            $update['description'] = $this->security->sanitize_user_input($_POST['description'], 'textarea');
        }
        // Check unfiltered_html capability for code content
        $can_unfiltered_html = current_user_can('unfiltered_html');

        // Support both traditional and chunked keys for content fields
        $raw_html = isset($_POST['html']) ? $_POST['html'] : ( $_POST['html_content'] ?? null );
        if (isset($raw_html)) {
            $update['html_content'] = is_string($raw_html)
                ? ($can_unfiltered_html ? wp_unslash($raw_html) : wp_kses_post(wp_unslash($raw_html)))
                : '';
        }

        $raw_css = isset($_POST['css']) ? $_POST['css'] : ( $_POST['css_content'] ?? null );
        if (isset($raw_css)) {
            // For non-unfiltered users we currently store empty CSS (safety)
            $update['css_content'] = is_string($raw_css)
                ? ($can_unfiltered_html ? wp_unslash($raw_css) : '')
                : '';
        }

        $raw_js = isset($_POST['js']) ? $_POST['js'] : ( $_POST['js_content'] ?? null );
        if (isset($raw_js)) {
            // For non-unfiltered users we currently store empty JS (safety)
            $update['js_content'] = is_string($raw_js)
                ? ($can_unfiltered_html ? wp_unslash($raw_js) : '')
                : '';
        }
        if (isset($_POST['json_spec'])) {
            $normalized_json = $this->handle_json_spec_for_save($_POST['json_spec']);
            
            // Check if normalization failed
            if (is_wp_error($normalized_json)) {
                wp_send_json_error(array(
                    'message' => __('Invalid JSON specification: ', 'talkgenai') . $normalized_json->get_error_message()
                ));
                return;
            }
            
            $update['json_spec'] = $normalized_json;
        }
        
        // If app_class or app_type provided, update them
        if (isset($_POST['app_class'])) {
            $update['app_class'] = sanitize_text_field($_POST['app_class']);
        }
        if (isset($_POST['app_type'])) {
            $update['app_type'] = sanitize_text_field($_POST['app_type']);
        }
        
        // Security analysis for JS if provided
        if (isset($update['js_content'])) {
            if ($this->security && method_exists($this->security, 'analyze_js_deny_list')) {
                $analysis = $this->security->analyze_js_deny_list($update['js_content']);
            } else {
                $analysis = array('is_safe' => true, 'violations' => array());
            }
            if (!$analysis['is_safe']) {
                wp_send_json_error(array(
                    'message' => __('JavaScript contains disallowed patterns.', 'talkgenai'),
                    'details' => $analysis['violations']
                ));
                return;
            }
        }
        
        if (empty($update)) {
            wp_send_json_error(array('message' => __('No valid fields to update.', 'talkgenai')));
            return;
        }
        
        // Delegate update to database layer so security_hash is recomputed when content changes
        $result = $this->database->update_app($app_id, $update);
        if (is_wp_error($result)) {
            // error_log('TalkGenAI: Database update failed: ' . $result->get_error_message());
            wp_send_json_error(array('message' => __('Failed to update app in database.', 'talkgenai')));
            return;
        }

        // Regenerate static files if content changed
        if (isset($update['css_content']) || isset($update['js_content']) || isset($update['html_content'])) {
            $app_data = $this->database->get_app($app_id);
            if ($app_data) {
                $file_results = $this->file_generator->generate_static_files($app_id, $app_data);
                
                // Update database with new file URLs and version
                if (!is_wp_error($file_results) && ($file_results['css_generated'] || $file_results['js_generated'])) {
                    $file_data = array();
                    if ($file_results['css_generated']) {
                        $file_data['css_file_url'] = $file_results['css_url'];
                    }
                    if ($file_results['js_generated']) {
                        $file_data['js_file_url'] = $file_results['js_url'];
                    }
                    $file_data['file_version'] = $file_results['version'];
                    
                    $this->database->update_app($app_id, $file_data);
                    
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        // error_log('TalkGenAI CHUNKED: Regenerated static files for app ' . $app_id);
                    }
                }
            }
        }

        // error_log("TalkGenAI: Successfully updated app ID: {$app_id}");
        
        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        
        wp_send_json_success(array(
            'message' => __('App updated successfully!', 'talkgenai'),
            'app_id' => $app_id
        ));
    }
    
    /**
     * Handle generate article AJAX request
     */
    public function handle_generate_article() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via check_ajax_referer() in calling context; inputs sanitized individually
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified via check_ajax_referer() in calling context; inputs sanitized individually
        // Increase timeout for article generation (3 minutes)
        // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Necessary for long-running AI article generation operations
        set_time_limit(180);
        
        try {
            $app_id = intval($_POST['app_id'] ?? 0);
            $length = isset($_POST['length']) ? sanitize_text_field(wp_unslash($_POST['length'])) : 'medium';
            $instructions = isset($_POST['instructions']) ? sanitize_textarea_field(wp_unslash($_POST['instructions'])) : '';
            
            if ($app_id <= 0) {
                wp_send_json_error(array('message' => __('Please select an app to generate an article for.', 'talkgenai')));
                return;
            }
            
            // Get app data
            $app = $this->database->get_app($app_id);
            if (!$app) {
                wp_send_json_error(array('message' => __('Selected app not found.', 'talkgenai')));
                return;
            }
            
            $app_title = $app['title'] ?? 'Your App';
            $app_description = $app['description'] ?? 'A useful web application';
            $app_spec = $app['json_spec'] ?? null;
            $app_url = isset($_POST['app_url']) ? esc_url_raw(wp_unslash($_POST['app_url'])) : '';
            $internal_link_candidates = function_exists('talkgenai_get_internal_link_candidates')
                ? talkgenai_get_internal_link_candidates($app_title, $app_description, 60)
                : array();
            
            // Parse JSON spec if it's a string
            if (is_string($app_spec)) {
                $app_spec = json_decode($app_spec, true);
            }
            
            // Call Python API to generate article
            $api_response = $this->api->generate_article(
                $app_id,
                $app_title,
                $app_description,
                $app_spec,
                $length,
                $instructions,
                $app_url,
                $internal_link_candidates
            );
            
            if (is_wp_error($api_response)) {
                // error_log('TalkGenAI: API error: ' . $api_response->get_error_message());
                
                // Check if this is an insufficient credits error
                if ($api_response->get_error_code() === 'insufficient_credits') {
                    $error_message = $api_response->get_error_message();
                    $error_data = $api_response->get_error_data();
                    $upgrade_url = isset($error_data['upgrade_url']) ? $error_data['upgrade_url'] : 'https://app.talkgen.ai/';
                    $credits_remaining = isset($error_data['credits_remaining']) ? $error_data['credits_remaining'] : 0;
                    $plan = isset($error_data['plan']) ? $error_data['plan'] : 'free';
                    
                    // Return structured error with HTML message and upgrade link
                    wp_send_json_error(array(
                        'message' => $error_message,
                        'error_code' => 'insufficient_credits',
                        'ai_message' => '<div class="talkgenai-error-notice" style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; padding: 16px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107; max-width: 500px;">
                            <div style="display: flex; align-items: center; margin-bottom: 12px;">
                                <span style="font-size: 22px; margin-right: 8px;">⚠️</span>
                                <strong style="color: #856404; font-size: 1.1em;">Out of Credits</strong>
                            </div>
                            <div style="color: #856404; margin-bottom: 12px;">
                                <p style="margin: 0 0 8px 0;">' . esc_html($error_message) . '</p>
                                <p style="margin: 0; font-size: 0.9em;">Credits Remaining: <strong>' . esc_html($credits_remaining) . '</strong></p>
                                <p style="margin: 0; font-size: 0.9em;">Current Plan: <strong>' . esc_html(ucfirst($plan)) . '</strong></p>
                            </div>
                            <div style="text-align: center; margin-top: 16px;">
                                <a href="' . esc_url($upgrade_url) . '" target="_blank" style="display: inline-block; padding: 12px 24px; background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: white; text-decoration: none; border-radius: 6px; font-weight: 600; transition: transform 0.2s;">
                                    Upgrade to Starter Plan →
                                </a>
                            </div>
                        </div>',
                        'is_html' => true
                    ));
                } else {
                    wp_send_json_error(array('message' => __('Failed to connect to article generation service. Please try again.', 'talkgenai')));
                }
                return;
            }
            
            // Check if API call was successful
            if (!isset($api_response['success']) || !$api_response['success']) {
                $error_message = $api_response['error'] ?? 'Unknown error occurred';
                
                // Check if this is a structured error with ai_message (e.g., insufficient credits from job system)
                if (isset($api_response['ai_message'])) {
                    // error_log('TalkGenAI: Article generation failed (structured error): ' . $error_message);
                    wp_send_json_error($api_response); // Pass through the complete structured error
                } else {
                    // error_log('TalkGenAI: Article generation failed: ' . $error_message);
                    wp_send_json_error(array('message' => __('Article generation failed: ', 'talkgenai') . $error_message));
                }
                return;
            }
            
            // Extract article content and meta description
            $article_data = $api_response['article'] ?? array();
            $article_content = $article_data['content'] ?? '';
            $meta_description = $article_data['meta_description'] ?? '';
            $faq_schema = $article_data['faq_schema'] ?? null;

            if (empty($article_content)) {
                wp_send_json_error(array('message' => __('Article was generated but content is empty. Please try again.', 'talkgenai')));
                return;
            }

            // Build response
            $response_data = array(
                'article' => array(
                    'content' => $article_content,
                    'meta_description' => $meta_description,
                    'app_title' => $article_data['app_title'] ?? $app_title,
                    'word_count' => $article_data['word_count'] ?? 0,
                    'article_length' => $article_data['article_length'] ?? $length,
                    'generated_at' => $article_data['generated_at'] ?? current_time('mysql')
                ),
                'server_response_time' => $api_response['server_response_time'] ?? 0
            );

            // Pass FAQ schema separately (not embedded in article HTML)
            if (!empty($faq_schema) && is_array($faq_schema)) {
                $response_data['faq_schema'] = $faq_schema;
            }

            wp_send_json_success($response_data);
            
        } catch (Exception $e) {
            // error_log('TalkGenAI: Article generation error: ' . $e->getMessage());
            wp_send_json_error(array('message' => __('Article generation failed. Please try again.', 'talkgenai')));
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }
    
    /**
     * Handle analyze website AJAX request
     */
    public function handle_analyze_website() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via check_ajax_referer() in calling context; inputs sanitized individually
        // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via check_ajax_referer() in calling context; inputs sanitized individually
        try {
            $website_url = isset($_POST['website_url']) ? esc_url_raw(wp_unslash($_POST['website_url'])) : '';
            $description = isset($_POST['description']) ? sanitize_textarea_field(wp_unslash($_POST['description'])) : '';
            
            // Require either URL or description (or both)
            if (empty($website_url) && empty($description)) {
                wp_send_json_error(array('message' => __('Please provide either a website URL or description (or both).', 'talkgenai')));
                return;
            }
            
            // Check if this is a premium feature by making a call to the API
            // The API will return 403 if user is on free tier
            $api_response = $this->api->analyze_website($website_url, $description);
            
            if (is_wp_error($api_response)) {
                // Check if this is a premium_feature error with structured data
                $error_code = $api_response->get_error_code();
                
                if ($error_code === 'premium_feature') {
                    // Pass through the structured error data for frontend handling
                    $error_data = $api_response->get_error_data();
                    wp_send_json_error(array_merge(
                        array(
                            'code' => 'premium_feature',
                            'error_code' => 'premium_feature',
                            'message' => __('This is a premium feature.', 'talkgenai')
                        ),
                        is_array($error_data) ? $error_data : array()
                    ));
                    return;
                }
                
                if ($error_code === 'unauthorized') {
                    // API key issue - return with proper code
                    wp_send_json_error(array(
                        'code' => 'unauthorized',
                        'error_code' => 'unauthorized',
                        'message' => $api_response->get_error_message()
                    ));
                    return;
                }
                
                // Standard error handling - include error code
                wp_send_json_error(array(
                    'code' => $error_code,
                    'message' => $api_response->get_error_message()
                ));
                return;
            }
            
            wp_send_json_success($api_response);
            
        } catch (Exception $e) {
            // error_log('TalkGenAI: Website analysis error: ' . $e->getMessage());
            wp_send_json_error(array('message' => __('Website analysis failed. Please try again.', 'talkgenai')));
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    }
    
    
    /**
     * Handle AJAX app loading
     */
    public function handle_load_app() {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class verify_ajax_nonce(); inputs sanitized via security class sanitize_user_input() or wp_unslash()
        // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via security class verify_ajax_nonce(); inputs sanitized via security class sanitize_user_input() or wp_unslash()
        // Security checks
        $this->security->verify_ajax_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])));
        $this->security->check_user_capability();
        
        $app_id = intval($_POST['app_id'] ?? 0);
        
        if (!$app_id) {
            wp_send_json_error(array(
                'message' => __('Invalid app ID.', 'talkgenai')
            ));
        }
        
        $app = talkgenai_get_app($app_id);
        
        if (!$app) {
            wp_send_json_error(array(
                'message' => __('App not found or access denied.', 'talkgenai')
            ));
        }
        
        // Ensure json_spec is valid before returning
        $raw_json_spec = $app['json_spec'] ?? '';
        $json_spec_obj = null;
        if ($raw_json_spec) {
            $decoded_spec = json_decode($raw_json_spec, true);
            // Check if spec is empty array OR has empty form fields and try to reconstruct from HTML
            $needs_reconstruction = (
                empty($decoded_spec) || // Empty array []
                (isset($decoded_spec['form']) && isset($decoded_spec['form']['fields']) && empty($decoded_spec['form']['fields'])) // Has form but empty fields
            );
            
            if ($needs_reconstruction || (isset($decoded_spec['_needs_reconstruction']) && $decoded_spec['_needs_reconstruction'])) {
                // error_log('TalkGenAI Load App: Reconstructing JSON spec from HTML and JavaScript');
                
                // Start with HTML-based reconstruction
                $reconstructed_spec = $this->reconstruct_json_spec_from_html($app, $decoded_spec);
                
                // Enhance with JavaScript-based calculation extraction
                $js_extracted = $this->extract_calculation_from_js($app['js_content'] ?? '');
                if ($js_extracted && $reconstructed_spec) {
                    // Merge JavaScript extractions into reconstructed spec
                    if (isset($js_extracted['directCalculation'])) {
                        $reconstructed_spec['directCalculation'] = $js_extracted['directCalculation'];
                    }
                    if (isset($js_extracted['resultsDisplay'])) {
                        $reconstructed_spec['resultsDisplay'] = $js_extracted['resultsDisplay'];
                    }
                    if (isset($js_extracted['uiText'])) {
                        $reconstructed_spec['uiText'] = $js_extracted['uiText'];
                    }
                    
                    // error_log('TalkGenAI Load App: Successfully enhanced spec with calculation logic from JavaScript');
                }
                
                if ($reconstructed_spec) {
                    // Remove reconstruction flag
                    unset($reconstructed_spec['_needs_reconstruction']);
                    $json_spec_obj = $reconstructed_spec;
                } else {
                    $json_spec_obj = $decoded_spec;
                }
            } else if (empty($decoded_spec)) {
                // error_log('TalkGenAI Load App: Detected empty json_spec, applying ensure_json_spec fix');
                $fixed_json_spec = $this->ensure_json_spec($raw_json_spec);
                $json_spec_obj = json_decode($fixed_json_spec, true);
            } else {
                // Return the original decoded spec
                $json_spec_obj = $decoded_spec;
            }
            
            // CRITICAL: Remove raw JavaScript code from json_spec before sending to frontend
            // The jsCode field contains raw JavaScript that corrupts JSON when saved back
            if (isset($json_spec_obj['directCalculation']['jsCode'])) {
                unset($json_spec_obj['directCalculation']['jsCode']);
                // error_log('TalkGenAI Load App: Removed raw JavaScript from json_spec to prevent corruption');
            }
        }
        
        wp_send_json_success(array(
            'app' => talkgenai_format_app_data($app),
            'html_content' => $app['html_content'],
            'json_spec' => $json_spec_obj
        ));
    }
    
    /**
     * Separate HTML and JavaScript content (same logic as database class)
     */
    private function separate_html_and_js($html_content) {
        $js_content = '';
        $html_only = $html_content;
        
        // Extract JavaScript content from script tags
        if (preg_match_all('/<script[^>]*>(.*?)<\/script>/is', $html_content, $matches)) {
            // Combine all JavaScript content, but clean it up
            $js_parts = array();
            foreach ($matches[1] as $js_part) {
                $cleaned_js = trim($js_part);
                if (!empty($cleaned_js)) {
                    $js_parts[] = $cleaned_js;
                }
            }
            $js_content = implode("\n\n", $js_parts);
            
            // Remove script tags from HTML
            $html_only = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $html_content);
        }
        
        // Clean up the JavaScript content
        if (!empty($js_content)) {
            // Remove any leading array literals that aren't assigned to variables
            $js_content = preg_replace('/^\s*\[.*?\]\s*\n/', '', $js_content);
            
            // Remove any trailing commas or semicolons that might cause issues
            $js_content = trim($js_content);
        }
        
        // Debug the extracted JavaScript
        if (defined('WP_DEBUG') && WP_DEBUG && !empty($js_content)) {
            // error_log('TalkGenAI: Raw extracted JS: ' . substr($js_content, 0, 500));
        }
        
        // Also extract inline event handlers and convert them
        $inline_events = array(
            'onclick', 'onload', 'onchange', 'onsubmit', 'onmouseover', 'onmouseout',
            'onfocus', 'onblur', 'onkeyup', 'onkeydown', 'onkeypress'
        );
        
        foreach ($inline_events as $event) {
            if (preg_match_all('/' . $event . '\s*=\s*["\']([^"\']*)["\']/', $html_only, $event_matches)) {
                // Add inline event code to JS content
                foreach ($event_matches[1] as $event_code) {
                    $js_content .= "\n// Inline " . $event . " handler\n" . $event_code;
                }
                
                // Remove inline event handlers from HTML
                $html_only = preg_replace('/' . $event . '\s*=\s*["\'][^"\']*["\']/', '', $html_only);
            }
        }
        
        return array(
            'html' => trim($html_only),
            'js' => trim($js_content)
        );
    }
    
    /**
     * Separate HTML, CSS, and JavaScript content
     */
    private function separate_html_css_and_js($html_content) {
        $css_content = '';
        $js_content = '';
        $html_only = $html_content;
        
        // Extract CSS content from style tags
        if (preg_match_all('/<style[^>]*>(.*?)<\/style>/is', $html_content, $css_matches)) {
            $css_parts = array();
            foreach ($css_matches[1] as $css_part) {
                $cleaned_css = trim($css_part);
                if (!empty($cleaned_css)) {
                    $css_parts[] = $cleaned_css;
                }
            }
            $css_content = implode("\n\n", $css_parts);
            
            // Remove style tags from HTML
            $html_only = preg_replace('/<style[^>]*>.*?<\/style>/is', '', $html_only);
        }
        
        // Extract JavaScript content from script tags
        if (preg_match_all('/<script[^>]*>(.*?)<\/script>/is', $html_only, $matches)) {
            $js_parts = array();
            foreach ($matches[1] as $js_part) {
                $cleaned_js = trim($js_part);
                if (!empty($cleaned_js)) {
                    $js_parts[] = $cleaned_js;
                }
            }
            $js_content = implode("\n\n", $js_parts);
            
            // Remove script tags from HTML
            $html_only = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $html_only);
        }
        
        // Clean up the JavaScript content
        if (!empty($js_content)) {
            $js_content = preg_replace('/^\s*\[.*?\]\s*\n/', '', $js_content);
            $js_content = trim($js_content);
        }
        
        // Extract inline event handlers
        $inline_events = array(
            'onclick', 'onload', 'onchange', 'onsubmit', 'onmouseover', 'onmouseout',
            'onfocus', 'onblur', 'onkeyup', 'onkeydown', 'onkeypress'
        );
        
        foreach ($inline_events as $event) {
            if (preg_match_all('/' . $event . '\s*=\s*["\']([^"\']*)["\']/', $html_only, $event_matches)) {
                foreach ($event_matches[1] as $event_code) {
                    $js_content .= "\n// Inline " . $event . " handler\n" . $event_code;
                }
                $html_only = preg_replace('/' . $event . '\s*=\s*["\'][^"\']*["\']/', '', $html_only);
            }
        }
        
        return array(
            'html' => trim($html_only),
            'css' => trim($css_content),
            'js' => trim($js_content            )
        );
        
        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    }
    
    /**
     * Get filtered apps with search and filters
     */
    private function get_filtered_apps($user_id, $search = '', $filter_type = '', $filter_scope = '', $filter_active = '', $filter_status = 'active', $sort_by = 'created_at', $sort_order = 'desc', $limit = 12, $offset = 0) {
        global $wpdb;
        
        $apps_table = esc_sql($this->database->get_apps_table());
        $where_conditions = ['user_id = %d'];
        $params = [$user_id];
        
        // Status filter
        if ($filter_status !== 'all') {
            $where_conditions[] = 'status = %s';
            $params[] = $filter_status;
        }
        
        // Search filter
        if (!empty($search)) {
            $where_conditions[] = '(title LIKE %s OR description LIKE %s)';
            $search_term = '%' . $wpdb->esc_like($search) . '%';
            $params[] = $search_term;
            $params[] = $search_term;
        }
        
        // Type filter
        if (!empty($filter_type)) {
            $where_conditions[] = 'app_type = %s';
            $params[] = $filter_type;
        }
        
        // Scope filter - DISABLED: scope column not yet in database schema
        // if (!empty($filter_scope)) {
        //     $where_conditions[] = 'scope = %s';
        //     $params[] = $filter_scope;
        // }
        
        // Active filter
        if ($filter_active !== '') {
            $where_conditions[] = 'active = %d';
            $params[] = intval($filter_active);
        }
        
        // Build WHERE clause
        $where_clause = 'WHERE ' . implode(' AND ', $where_conditions);
        
        // Validate sort columns (whitelist approach for security)
        $allowed_sort_columns = ['created_at', 'updated_at', 'title', 'app_type'];
        if (!in_array($sort_by, $allowed_sort_columns, true)) {
            $sort_by = 'created_at';
        }
        
        // Validate and escape sort order (whitelist approach)
        $sort_order = strtoupper($sort_order) === 'ASC' ? 'ASC' : 'DESC';
        
        // Build final query with escaped table name and validated sort parameters
        $sql = "SELECT * FROM " . esc_sql($apps_table) . " 
                {$where_clause} 
                ORDER BY {$sort_by} {$sort_order} 
                LIMIT %d OFFSET %d";
        
        $params[] = $limit;
        $params[] = $offset;
        
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is escaped with esc_sql(); sort columns are whitelisted; WHERE clause uses parameterized values; Direct query needed for complex filtered apps list with dynamic WHERE clause
        return $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
    }
    
    /**
     * Get count of filtered apps
     */
    private function get_filtered_apps_count($user_id, $search = '', $filter_type = '', $filter_scope = '', $filter_active = '', $filter_status = 'active') {
        global $wpdb;
        
        $apps_table = esc_sql($this->database->get_apps_table());
        $where_conditions = ['user_id = %d'];
        $params = [$user_id];
        
        // Status filter
        if ($filter_status !== 'all') {
            $where_conditions[] = 'status = %s';
            $params[] = $filter_status;
        }
        
        // Search filter
        if (!empty($search)) {
            $where_conditions[] = '(title LIKE %s OR description LIKE %s)';
            $search_term = '%' . $wpdb->esc_like($search) . '%';
            $params[] = $search_term;
            $params[] = $search_term;
        }
        
        // Type filter
        if (!empty($filter_type)) {
            $where_conditions[] = 'app_type = %s';
            $params[] = $filter_type;
        }
        
        // Scope filter - DISABLED: scope column not yet in database schema
        // if (!empty($filter_scope)) {
        //     $where_conditions[] = 'scope = %s';
        //     $params[] = $filter_scope;
        // }
        
        // Active filter
        if ($filter_active !== '') {
            $where_conditions[] = 'active = %d';
            $params[] = intval($filter_active);
        }
        
        // Build WHERE clause
        $where_clause = 'WHERE ' . implode(' AND ', $where_conditions);
        
        $sql = "SELECT COUNT(*) FROM " . esc_sql($apps_table) . " {$where_clause}";
        
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is escaped with esc_sql(); WHERE clause uses parameterized values; Direct query needed for counting filtered apps with dynamic WHERE clause
        return intval($wpdb->get_var($wpdb->prepare($sql, $params)));
    }
    
    /**
     * Get unique app types for filter dropdown
     */
    private function get_unique_app_types($user_id) {
        global $wpdb;
        
        $apps_table = esc_sql($this->database->get_apps_table());
        
        $sql = "SELECT app_class, app_type, COUNT(*) as count 
                FROM " . esc_sql($apps_table) . " 
                WHERE user_id = %d AND status = 'active'
                GROUP BY app_class, app_type 
                ORDER BY count DESC, app_type ASC";
        
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is escaped with esc_sql(); Direct query needed for aggregating unique app types for filter dropdown
        return $wpdb->get_results($wpdb->prepare($sql, $user_id), ARRAY_A);
    }
    
    /**
     * Get unique scopes for filter dropdown
     * NOTE: Scope column not yet implemented in database schema - returning empty array for now
     */
    private function get_unique_scopes($user_id) {
        // TODO: Add 'scope' column to database schema in future version
        // For now, return empty array to prevent database errors
        return array();
    }
    
    /**
     * Handle bulk actions
     */
    private function handle_bulk_actions() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified immediately below before any action is taken
        if (!isset($_POST['action']) || $_POST['action'] === '-1') {
            return;
        }
        
        // Security check
        if (!isset($_POST['talkgenai_bulk_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['talkgenai_bulk_nonce'])), 'talkgenai_bulk_action')) {
            wp_die(esc_html__('Security check failed.', 'talkgenai'));
        }
        
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above
        $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
        $app_ids = array_map('intval', $_POST['app_ids'] ?? array());
        
        if (empty($app_ids)) {
            return;
        }
        
        $processed = 0;
        
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above
        foreach ($app_ids as $app_id) {
            if ($action === 'delete') {
                    // Delete static files before deleting database record
                    if (isset($this->file_generator)) {
                        $this->file_generator->delete_static_files($app_id);
                    }
                    
                    $result = $this->database->delete_app($app_id);
                    if (!is_wp_error($result)) {
                        $processed++;
                    }
            }
        }
        
        if ($processed > 0) {
            $message = sprintf(
                /* translators: %d: number of apps processed */
                _n('%d app processed.', '%d apps processed.', $processed, 'talkgenai'),
                $processed
            );
            add_settings_error('talkgenai_bulk_action', 'bulk_success', $message, 'success');
        }
    }
    
    /**
     * Render edit app page
     * Reuse the generate page layout/JS; preload the app so admin.js switches to modify mode.
     */
    private function render_edit_app_page() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for display only, capability check enforced
        $app_id = intval($_GET['app_id'] ?? 0);
        $app = talkgenai_get_app($app_id);

        if (!$app) {
            wp_die(esc_html__('App not found or access denied.', 'talkgenai'));
        }

        $server_status = $this->api->get_server_status();
        
        // Ensure json_spec is valid before passing to JavaScript
        $raw_json_spec = $app['json_spec'] ?? '';
        if ($raw_json_spec) {
            $decoded_spec = json_decode($raw_json_spec, true);
            
            
            // Check if spec is empty array OR has empty form fields and try to reconstruct from HTML
            $needs_reconstruction = (
                empty($decoded_spec) || // Empty array []
                (isset($decoded_spec['form']) && isset($decoded_spec['form']['fields']) && empty($decoded_spec['form']['fields'])) // Has form but empty fields
            );
            
            if ($needs_reconstruction || (isset($decoded_spec['_needs_reconstruction']) && $decoded_spec['_needs_reconstruction'])) {
                // error_log('TalkGenAI Edit Page: Reconstructing JSON spec from HTML and JavaScript');
                
                // Start with HTML-based reconstruction
                $reconstructed_spec = $this->reconstruct_json_spec_from_html($app, $decoded_spec);
                
                // Enhance with JavaScript-based calculation extraction
                $js_extracted = $this->extract_calculation_from_js($app['js_content'] ?? '');
                if ($js_extracted && $reconstructed_spec) {
                    // Merge JavaScript extractions into reconstructed spec
                    if (isset($js_extracted['directCalculation'])) {
                        $reconstructed_spec['directCalculation'] = $js_extracted['directCalculation'];
                    }
                    if (isset($js_extracted['resultsDisplay'])) {
                        $reconstructed_spec['resultsDisplay'] = $js_extracted['resultsDisplay'];
                    }
                    if (isset($js_extracted['uiText'])) {
                        $reconstructed_spec['uiText'] = $js_extracted['uiText'];
                    }
                    
                    // error_log('TalkGenAI Edit Page: Successfully enhanced spec with calculation logic from JavaScript');
                }
                
                if ($reconstructed_spec) {
                    // Remove reconstruction flag
                    unset($reconstructed_spec['_needs_reconstruction']);
                    $json_spec_obj = $reconstructed_spec;
                } else {
                    $json_spec_obj = $decoded_spec;
                }
            } else if (empty($decoded_spec)) {
                // error_log('TalkGenAI Edit Page: Detected empty json_spec, applying ensure_json_spec fix');
                $fixed_json_spec = $this->ensure_json_spec($raw_json_spec);
                $json_spec_obj = json_decode($fixed_json_spec, true);
            } else {
                // Return the original decoded spec
                $json_spec_obj = $decoded_spec;
            }
            
            // CRITICAL: Remove raw JavaScript code from json_spec before sending to frontend
            // The jsCode field contains raw JavaScript that corrupts JSON when saved back
            if (isset($json_spec_obj['directCalculation']['jsCode'])) {
                unset($json_spec_obj['directCalculation']['jsCode']);
                // error_log('TalkGenAI Edit Page: Removed raw JavaScript from json_spec to prevent corruption');
            }
        } else {
            $json_spec_obj = null;
        }
        ?>
        <div class="wrap">
            <div class="talkgenai-header-bar">
                <h1><?php esc_html_e('Edit App', 'talkgenai'); ?></h1>
                <div class="talkgenai-header-actions">
                    <div class="talkgenai-all-buttons">
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-settings')); ?>" class="button">
                            <?php esc_html_e('Settings', 'talkgenai'); ?>
                        </a>
                        <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-apps')); ?>" class="button">
                            <?php esc_html_e('View All Apps', 'talkgenai'); ?>
                        </a>
                        <button type="button" class="button" id="generate-new-btn-header" style="display: inline-block;">
                            <?php esc_html_e('Generate New', 'talkgenai'); ?>
                        </button>
                    </div>
                    <div class="talkgenai-server-status-compact">
                        <?php echo wp_kses_post(talkgenai_get_server_status_indicator($server_status)); ?>
                        <span class="talkgenai-server-mode">
                            <?php echo talkgenai_is_local_server() ? esc_html__('Local', 'talkgenai') : esc_html__('Remote', 'talkgenai'); ?>
                        </span>
                    </div>
                </div>
            </div>

            <div class="talkgenai-admin-container">
                <div class="talkgenai-main-content">
                    <!-- Same workspace layout as generate page -->
                    <div class="talkgenai-app-workspace">
                        <!-- Left: Generation/Chat Form -->
                        <div class="talkgenai-generation-section">
                            <div class="talkgenai-generation-form">
                                <h3><?php esc_html_e('Describe Your App', 'talkgenai'); ?></h3>
                                <form id="talkgenai-generate-form">
                                    <table class="form-table">
                                        <tr>
                                            <td colspan="2">
                                                <textarea 
                                                    id="app_description" 
                                                    name="description" 
                                                    rows="6" 
                                                    class="large-text"
                                                    placeholder="<?php esc_attr_e('Describe the changes you want to make...', 'talkgenai'); ?>"
                                                    required
                                                ></textarea>
                                                <p class="description">
                                                    <?php esc_html_e('Tell the AI what to change. Press Enter in chat to send.', 'talkgenai'); ?>
                                                </p>
                                            </td>
                                        </tr>
                                    </table>

                                    <!-- Generate button not needed on edit page -->
                                </form>
                            </div>

                            <!-- Action buttons (same block as generate page) -->
                            <div class="talkgenai-form-actions" style="display: block;">
                                <button type="button" class="button button-primary" id="save-app-btn">
                                    <?php esc_html_e('Save App', 'talkgenai'); ?>
                                </button>
                            </div>
                        </div>

                        <!-- Right: Preview -->
                        <div class="talkgenai-preview-section">
                            <div id="talkgenai-preview-area" style="display: block;">
                                <h3><?php esc_html_e('App Preview', 'talkgenai'); ?></h3>
                                <div id="talkgenai-preview-container">
                                    <?php 
                                    // Enqueue preview styles properly using wp_add_inline_style()
                                    if (!empty($app['css_content'])) {
                                        // Add inline CSS to the talkgenai-admin style handle (already enqueued)
                                        wp_add_inline_style('talkgenai-admin', wp_strip_all_tags($app['css_content']));
                                    }
                                    
                                    if (!empty($app['html_content'])) {
                                        // User-generated HTML content - sanitized with wp_kses_post() to allow safe HTML tags
                                        echo wp_kses_post($app['html_content']); 
                                    } else {
                                        echo '<div style="padding: 20px; text-align: center; color: #666;">' . esc_html__('No app content found. App ID:', 'talkgenai') . ' ' . absint($app['id']) . '</div>';
                                    }
                                    ?>
                                </div>
                            </div>

                            <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display of existing app data ?>
                            <div id="talkgenai-preview-placeholder" class="talkgenai-generation-form" style="display:none;"></div>
                        </div>
                    </div>
                </div>
                <div class="talkgenai-sidebar">
                    <div class="talkgenai-sidebar-box">
                        <h3><?php esc_html_e('Recent Apps', 'talkgenai'); ?></h3>
                        <?php $this->render_recent_apps(); ?>
                    </div>
                </div>
            </div>
        </div>

        <?php
        // Edit page scripts are now loaded via wp_add_inline_script() in enqueue_admin_assets()
        // This follows WordPress best practices for including page-specific JavaScript
        ?>
        <?php
    }
    
    /**
     * Render preview app page
     */
    private function render_preview_app_page() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page URL parameter for display only, capability check enforced
        $app_id = intval($_GET['app_id'] ?? 0);
        $app = talkgenai_get_app($app_id);
        
        if (!$app) {
            wp_die(esc_html__('App not found or access denied.', 'talkgenai'));
        }
        
        $formatted_app = talkgenai_format_app_data($app);
        // Mark CSP required on admin preview
        if (class_exists('TalkGenAI_Security')) {
            TalkGenAI_Security::require_csp_for_request();
        }
        ?>
        <div class="wrap">
            <?php /* translators: %s: app title */ ?>
            <h1><?php printf(esc_html__('Preview: %s', 'talkgenai'), esc_html($formatted_app['title'])); ?></h1>
            
            <div class="talkgenai-preview-full">
                <?php 
                // Use shortcode to properly handle HTML and JavaScript separation
                echo do_shortcode('[talkgenai_app id="' . $app['id'] . '"]'); 
                ?>
            </div>
            
            <div class="talkgenai-preview-actions">
                <a href="<?php echo esc_url(talkgenai_get_app_edit_url($app['id'])); ?>" class="button button-primary">
                    <?php esc_html_e('Edit App', 'talkgenai'); ?>
                </a>
                <a href="<?php echo esc_url(admin_url('admin.php?page=talkgenai-apps')); ?>" class="button">
                    <?php esc_html_e('Back to Apps', 'talkgenai'); ?>
                </a>
            </div>
        </div>
        <?php
    }
    
    /**
     * Recursively decode Unicode escapes in array/object data
     * Fixes issues where Hebrew/Unicode text becomes u05xx escapes in JSON spec
     */
    private function decode_unicode_in_spec($data) {
        if (is_string($data)) {
            // Check if string contains Unicode escape sequences
            if (strpos($data, 'u05') !== false || strpos($data, 'u06') !== false || preg_match('/u[0-9a-fA-F]{4}/', $data)) {
                // Decode Unicode escape sequences  
                $decoded = preg_replace_callback('/u([0-9a-fA-F]{4})/', function($matches) {
                    return html_entity_decode('&#x' . $matches[1] . ';', ENT_QUOTES, 'UTF-8');
                }, $data);
                // error_log('TalkGenAI UNICODE FIX: Decoded "' . $data . '" to "' . $decoded . '"');
                return $decoded;
            }
            return $data;
        } elseif (is_array($data)) {
            $result = array();
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Helper function for data processing, no direct POST access
            foreach ($data as $key => $value) {
                $result[$key] = $this->decode_unicode_in_spec($value);
            }
            return $result;
        } else {
            return $data;
        }
    }

    /**
     * Handle JSON spec for save operations (both new and update)
     * Handles WordPress AJAX escaping issues
     */
    /**
     * Normalize json_spec for database storage
     * Simple, clean approach - fixes root cause instead of symptoms
     */
    private function normalize_json_spec($json_spec_raw) {
        // Step 1: Handle non-string input
        if (!is_string($json_spec_raw)) {
            // Already an array/object, just encode it
            return wp_json_encode($json_spec_raw);
        }
        
        // Step 2: Undo WordPress automatic slashing (root cause #1)
        // WordPress adds slashes to POST data automatically
        // CRITICAL: Skip wp_unslash for chunked saves (data from JSON POST, never slashed)
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Helper function called after nonce verification in AJAX handlers
        $is_chunked_save = isset($_POST['_is_chunked_save']) && $_POST['_is_chunked_save'] === true;
        
        if ($is_chunked_save) {
            // Data from JSON POST (php://input) - never slashed, don't unslash
            $json_string = $json_spec_raw;
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // error_log("TalkGenAI NORMALIZE: Skipping wp_unslash for chunked save data");
            }
        } else {
            // Data from FormData POST - WordPress slashed it, need to unslash
            $json_string = wp_unslash($json_spec_raw);
        }
        
        // DEBUG: Check encoding before and after wp_unslash
        if (defined('WP_DEBUG') && WP_DEBUG) {
            $encoding_before = mb_detect_encoding($json_spec_raw, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true);
            $encoding_after = mb_detect_encoding($json_string, ['UTF-8', 'ISO-8859-1', 'Windows-1252'], true);
            // error_log("TalkGenAI NORMALIZE: Encoding BEFORE wp_unslash: {$encoding_before}");
            // error_log("TalkGenAI NORMALIZE: Encoding AFTER wp_unslash: {$encoding_after}");
            // error_log("TalkGenAI NORMALIZE: Length BEFORE wp_unslash: " . strlen($json_spec_raw));
            // error_log("TalkGenAI NORMALIZE: Length AFTER wp_unslash: " . strlen($json_string));
        }
        
        // Step 3: Decode to validate and normalize
        $decoded = json_decode($json_string, true);
        
        // Step 4: Validate
        if (json_last_error() !== JSON_ERROR_NONE) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // error_log('TalkGenAI: Invalid JSON in normalize_json_spec: ' . json_last_error_msg());
                // error_log('TalkGenAI: JSON preview (first 300): ' . substr($json_string, 0, 300));
                // error_log('TalkGenAI: JSON tail (last 300): ' . substr($json_string, -300));
                
                // Try to find the exact position of the syntax error
                $json_lines = explode("\n", $json_string);
                $line_count = count($json_lines);
                // error_log("TalkGenAI: JSON has {$line_count} lines");
                
                // Check for common corruption patterns
                if (strpos($json_string, '\\"') !== false) {
                    // error_log('TalkGenAI: WARNING - Found escaped quotes (\") in JSON - wp_unslash may have corrupted data');
                }
            }
            return new WP_Error('invalid_json', 'Invalid JSON specification: ' . json_last_error_msg());
        }
        
        // Step 5: Re-encode cleanly using WordPress function
        return wp_json_encode($decoded);
    }
    
    /**
     * DEPRECATED - Use normalize_json_spec() instead
     * Kept for backward compatibility
     */
    private function handle_json_spec_for_save($json_spec_raw) {
        return $this->normalize_json_spec($json_spec_raw);
    }


    
    /**
     * Reconstruct json_spec from HTML content when form fields are missing
     */
    private function reconstruct_json_spec_from_html($app, $base_spec) {
        $html_content = $app['html_content'] ?? '';
        if (empty($html_content)) {
            return null;
        }
        
        // Parse HTML to extract form fields
        $fields = array();
        
        // Use DOMDocument to parse HTML safely
        $dom = new DOMDocument();
        libxml_use_internal_errors(true);
        $dom->loadHTML('<?xml encoding="UTF-8">' . $html_content);
        libxml_clear_errors();
        
        $xpath = new DOMXPath($dom);
        
        // Find all input, select, and textarea elements
        $form_elements = $xpath->query('//input | //select | //textarea');
        
        foreach ($form_elements as $element) {
            $id = $element->getAttribute('id');
            $name = $element->getAttribute('name') ?: $id;
            $type = $element->getAttribute('type') ?: $element->tagName;
            
            if (empty($id) || $id === 'calculateBtn') {
                continue; // Skip elements without ID or buttons
            }
            
            $field = array(
                'id' => $id,
                'type' => $this->map_html_type_to_field_type($type, $element),
                'label' => $this->extract_label_for_field($xpath, $id),
                'required' => $element->hasAttribute('required')
            );
            
            // Add additional properties based on type
            if ($type === 'number') {
                if ($element->hasAttribute('min')) {
                    $field['min'] = $element->getAttribute('min');
                }
                if ($element->hasAttribute('max')) {
                    $field['max'] = $element->getAttribute('max');
                }
                if ($element->hasAttribute('value')) {
                    $field['value'] = $element->getAttribute('value');
                }
            }
            
            // Extract options for select elements
            if ($element->tagName === 'select') {
                $options = array();
                $option_elements = $xpath->query('.//option', $element);
                foreach ($option_elements as $option) {
                    $options[] = array(
                        'value' => $option->getAttribute('value'),
                        'text' => trim($option->textContent)
                    );
                }
                if (!empty($options)) {
                    $field['options'] = $options;
                }
            }
            
            $fields[] = $field;
        }
        
        if (empty($fields)) {
            return null;
        }
        
        // Create reconstructed spec based on the base spec (or create new if base is empty)
        if (empty($base_spec)) {
            $reconstructed = array(
                'appClass' => $app['app_class'] ?? 'calculator',
                'appType' => $app['app_type'] ?? 'calculator_form',
                'page' => array('title' => 'App', 'description' => 'Generated app'),
                'form' => array('fields' => $fields),
                'styling' => array('theme' => 'professional')
            );
        } else {
            // CRITICAL FIX: Preserve ALL existing spec data, only update form fields
            $reconstructed = $base_spec;
            
            // Only update form fields if we have better data from HTML
            if (!empty($fields) && isset($reconstructed['form'])) {
                $reconstructed['form']['fields'] = $fields;
            }
            
            // Ensure critical calculation components are preserved
            if (!isset($reconstructed['directCalculation']) && isset($base_spec['directCalculation'])) {
                $reconstructed['directCalculation'] = $base_spec['directCalculation'];
            }
            if (!isset($reconstructed['resultsDisplay']) && isset($base_spec['resultsDisplay'])) {
                $reconstructed['resultsDisplay'] = $base_spec['resultsDisplay'];
            }
            if (!isset($reconstructed['uiText']) && isset($base_spec['uiText'])) {
                $reconstructed['uiText'] = $base_spec['uiText'];
            }
        }
        
        // Try to extract title and description from HTML
        $title_element = $xpath->query('//h1 | //h2 | //*[@class="calculator-title"] | //*[@class="title"]')->item(0);
        if ($title_element) {
            $reconstructed['page']['title'] = trim($title_element->textContent);
        }
        
        $desc_element = $xpath->query('//*[@class="calculator-description"] | //*[@class="description"] | //p[1]')->item(0);
        if ($desc_element) {
            $reconstructed['page']['description'] = trim($desc_element->textContent);
        }
        
        return $reconstructed;
    }
    
    /**
     * Extract calculation logic and results display from JavaScript content
     * Used when JSON spec is incomplete but JavaScript contains the full logic
     */
    private function extract_calculation_from_js($js_content) {
        if (empty($js_content)) {
            return null;
        }
        
        $extracted = array();
        
        // Extract direct calculation JavaScript code
        // Look for the main calculation function
        if (preg_match('/function calculate\(\)\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/', $js_content, $matches)) {
            $calculation_code = trim($matches[1]);
            if (!empty($calculation_code)) {
                // Clean up the code - remove widget detection boilerplate
                $calculation_code = preg_replace('/const widget = [^;]+;[^}]*if \(!widget\) return;/', '', $calculation_code);
                $calculation_code = trim($calculation_code);
                
                if (!empty($calculation_code)) {
                    $extracted['directCalculation'] = array(
                        'jsCode' => 'function calculate() { ' . $calculation_code . ' }'
                    );
                }
            }
        }
        
        // Extract result field names from JavaScript
        // Look for patterns like widget.querySelector('#result_fieldName')
        if (preg_match_all('/widget\.querySelector\([\'"]#result_([a-zA-Z0-9_]+)[\'"]/', $js_content, $matches)) {
            $result_fields = array_unique($matches[1]);
            
            if (!empty($result_fields)) {
                $result_items = array();
                foreach ($result_fields as $field) {
                    // Convert camelCase to title case for labels
                    $label = ucfirst(preg_replace('/([A-Z])/', ' $1', $field));
                    $result_items[] = array(
                        'label' => $label,
                        'value' => $field
                    );
                }
                
                $extracted['resultsDisplay'] = array(
                    'title' => 'Results',
                    'sections' => array(
                        array(
                            'type' => 'summary_cards',
                            'items' => $result_items
                        )
                    )
                );
            }
        }
        
        // Extract UI text from JavaScript
        // Look for button text and other UI elements
        $ui_text = array('labels' => array());
        
        // Default button text
        $ui_text['labels']['calculateButton'] = 'Calculate';
        $ui_text['labels']['calculatorTitle'] = 'Calculator';
        
        if (!empty($ui_text['labels'])) {
            $extracted['uiText'] = $ui_text;
        }
        
        return !empty($extracted) ? $extracted : null;
    }
    
    /**
     * Map HTML input types to field types
     */
    private function map_html_type_to_field_type($html_type, $element) {
        switch ($html_type) {
            case 'number':
                return 'number';
            case 'select':
                return 'select';
            case 'textarea':
                return 'textarea';
            case 'checkbox':
                return 'checkbox';
            case 'radio':
                return 'radio';
            case 'email':
                return 'email';
            case 'tel':
                return 'tel';
            case 'url':
                return 'url';
            case 'date':
                return 'date';
            case 'text':
            default:
                return 'text';
        }
    }
    
    /**
     * Extract label text for a form field
     */
    private function extract_label_for_field($xpath, $field_id) {
        // Look for label with for attribute
        $label = $xpath->query('//label[@for="' . $field_id . '"]')->item(0);
        if ($label) {
            return trim($label->textContent);
        }
        
        // Look for label containing the input
        $label = $xpath->query('//label[.//*[@id="' . $field_id . '"]]')->item(0);
        if ($label) {
            return trim($label->textContent);
        }
        
        // Look for nearby text with class "form-label"
        $label = $xpath->query('//*[@class="form-label" and following-sibling::*[@id="' . $field_id . '"]]')->item(0);
        if ($label) {
            return trim($label->textContent);
        }
        
        // Fallback: use field ID as label
        return ucwords(str_replace(['_', '-'], ' ', $field_id));
    }
    
    /**
     * Hide non-critical admin notices on TalkGenAI pages for clean interface
     * 
     * WORDPRESS.ORG COMPLIANT - Selective hiding approach:
     * - Hides: Update notices, promotions, success messages (clutter)
     * - Keeps visible: Errors, warnings, security notices (critical)
     * - Only affects TalkGenAI pages (not global)
     * - Uses CSS only (no hook removal)
     * 
     * This provides a clean interface while maintaining safety compliance.
     */
    public function hide_unimportant_notices_on_talkgenai_pages() {
        $screen = get_current_screen();
        
        // Only apply on TalkGenAI admin pages
        if (!$screen || strpos($screen->id, 'talkgenai') === false) {
            return;
        }
        
        // Selective CSS hiding - only hides non-critical notices
        // Critical notices (errors, warnings) remain visible for safety
        $css = '
            /* Hide non-critical admin notices on TalkGenAI pages for clean UI */
            .notice.updated,
            .notice.notice-success:not([class*="talkgenai"]), 
            .notice.notice-info:not([class*="talkgenai"]),
            .update-nag:not([class*="talkgenai"]),
            .notice[data-dismissible]:not(.notice-error):not(.notice-warning):not([class*="talkgenai"]) {
                display: none !important;
            }
            
            /* ALWAYS keep critical notices visible (WordPress.org compliance) */
            .notice.error,
            .notice.notice-error,
            .notice.notice-warning {
                display: block !important;
            }
            
            /* Always keep TalkGenAI own notices visible */
            .notice.talkgenai-notice,
            .notice[class*="talkgenai"] {
                display: block !important;
            }
        ';
        
        // Use WordPress best practice: wp_add_inline_style()
        wp_add_inline_style('talkgenai-admin-base', $css);
    }

    /**
     * Sanitize HTML like wp_kses_post but also allow schema.org microdata attributes
     * (itemscope, itemtype, itemprop) so FAQ structured data is preserved.
     */
    private function kses_post_with_schema( $content ) {
        $allowed = wp_kses_allowed_html( 'post' );

        $schema_attrs = array(
            'itemscope' => true,
            'itemtype'  => true,
            'itemprop'  => true,
        );

        // Add schema attributes to all allowed tags
        foreach ( $allowed as $tag => $attrs ) {
            if ( is_array( $attrs ) ) {
                $allowed[ $tag ] = array_merge( $attrs, $schema_attrs );
            }
        }

        return wp_kses( $content, $allowed );
    }

    /**
     * Handle Create Draft AJAX request
     * Creates a WordPress draft post/page from generated article content
     */
    public function handle_create_draft() {
        // Verify nonce (also checked by caller ajax_create_draft, but PHPCS requires it per-function)
        check_ajax_referer('talkgenai_nonce', 'nonce');

        $post_type     = isset($_POST['post_type']) ? sanitize_key(wp_unslash($_POST['post_type'])) : '';
        $title         = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
        $meta_desc     = isset($_POST['meta_description']) ? sanitize_text_field(wp_unslash($_POST['meta_description'])) : '';
        $focus_keyword = isset($_POST['focus_keyword']) ? sanitize_text_field(wp_unslash($_POST['focus_keyword'])) : '';

        // Sanitize HTML content using wp_kses with schema.org microdata attributes preserved
        $allowed_html = wp_kses_allowed_html('post');
        $schema_attrs = array('itemscope' => true, 'itemtype' => true, 'itemprop' => true);
        foreach ($allowed_html as $tag => $attrs) {
            if (is_array($attrs)) {
                $allowed_html[$tag] = array_merge($attrs, $schema_attrs);
            }
        }
        $safe_content = isset($_POST['content']) ? wp_kses(wp_unslash($_POST['content']), $allowed_html) : '';

        // FAQ schema: accept as raw JSON string, validate via json_decode + re-encode safely.
        // Saved as post meta and output via wp_head hook (not embedded in post_content,
        // because wp_kses_post strips <script> tags and shows raw JSON as visible text).
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below via json_decode validation + wp_json_encode re-encoding
        $faq_schema_raw = isset($_POST['faq_schema']) ? wp_unslash($_POST['faq_schema']) : '';

        // Validate post type
        if (!in_array($post_type, array('post', 'page'), true)) {
            wp_send_json_error(array('message' => __('Invalid post type. Must be "post" or "page".', 'talkgenai')));
        }

        // Validate required fields
        if (empty($title)) {
            wp_send_json_error(array('message' => __('Title is required.', 'talkgenai')));
        }
        if (empty($safe_content)) {
            wp_send_json_error(array('message' => __('Article content is required.', 'talkgenai')));
        }

        // FAQ schema is NOT embedded in post_content (Gutenberg corrupts <script> tags,
        // showing raw JSON as visible text). Instead it is saved as post meta and
        // output cleanly via the wp_head hook in output_schema_markup().

        // Strip any leftover AI image placeholder figures (safety net — JS should have removed these).
        $draft_content = preg_replace('/<figure[^>]*data-tgai-image-placeholder[^>]*>.*?<\/figure>/is', '', $safe_content);

        // Create draft post/page
        $post_data = array(
            'post_title'   => $title,
            'post_content' => $draft_content,
            'post_status'  => 'draft',
            'post_type'    => $post_type,
            'post_author'  => get_current_user_id(),
        );

        $post_id = wp_insert_post($post_data, true);

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

        // Set featured image and inject article image if an attachment ID was provided.
        $attachment_id = isset($_POST['attachment_id']) ? absint(wp_unslash($_POST['attachment_id'])) : 0;
        if ($attachment_id && get_post($attachment_id)) {
            set_post_thumbnail($post_id, $attachment_id);

            // Build a standard WordPress <img> tag and insert it before the first <h2>.
            // Format mirrors WordPress classic editor output: aligncenter wp-image-{id}, scaled to max 690px wide.
            $img_url  = wp_get_attachment_url($attachment_id);
            $img_alt  = (string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
            // Fallback alt text: use post title if media library alt is somehow empty.
            if (!$img_alt) {
                $img_alt = $title;
            }
            $img_meta  = wp_get_attachment_metadata($attachment_id);
            $native_w  = !empty($img_meta['width'])  ? (int) $img_meta['width']  : 0;
            $native_h  = !empty($img_meta['height']) ? (int) $img_meta['height'] : 0;
            $max_w     = 690;
            if ($native_w > 0 && $native_h > 0 && $native_w > $max_w) {
                $display_w = $max_w;
                $display_h = (int) round($max_w * $native_h / $native_w);
            } elseif ($native_w > 0 && $native_h > 0) {
                $display_w = $native_w;
                $display_h = $native_h;
            } else {
                $display_w = $max_w;
                $display_h = 460;
            }

            $img_html = '<img class="aligncenter wp-image-' . $attachment_id . '"'
                . ' title="' . esc_attr($img_alt) . '"'
                . ' src="' . esc_url($img_url) . '"'
                . ' alt="' . esc_attr($img_alt) . '"'
                . ' width="' . $display_w . '"'
                . ' height="' . $display_h . '"'
                . ' />';

            // Insert before first <h2>, or prepend if the article has none.
            if (preg_match('/<h2[\s>]/i', $draft_content)) {
                $draft_content = preg_replace('/<h2[\s>]/i', $img_html . '<h2 ', $draft_content, 1);
            } else {
                $draft_content = $img_html . $draft_content;
            }

            wp_update_post(array(
                'ID'           => $post_id,
                'post_content' => $draft_content,
            ));
        }

        // Save FAQ schema as post meta (output via wp_head hook in output_schema_markup)
        if (!empty($faq_schema_raw)) {
            $faq_decoded = json_decode($faq_schema_raw, true);
            if (json_last_error() === JSON_ERROR_NONE && is_array($faq_decoded)) {
                $schema_json = wp_json_encode($faq_decoded, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                update_post_meta($post_id, '_talkgenai_schema_markup', array($schema_json));
            }
        }

        // Set SEO meta description if a supported plugin is active
        if (!empty($meta_desc)) {
            // Yoast SEO
            if (defined('WPSEO_VERSION')) {
                update_post_meta($post_id, '_yoast_wpseo_metadesc', $meta_desc);
            }
            // RankMath
            if (defined('RANK_MATH_VERSION')) {
                update_post_meta($post_id, 'rank_math_description', $meta_desc);
            }
        }

        // Set focus keyphrase (primary keyword) if a supported SEO plugin is active
        if (!empty($focus_keyword)) {
            // Yoast SEO
            if (defined('WPSEO_VERSION')) {
                update_post_meta($post_id, '_yoast_wpseo_focuskw', $focus_keyword);
            }
            // RankMath
            if (defined('RANK_MATH_VERSION')) {
                update_post_meta($post_id, 'rank_math_focus_keyword', $focus_keyword);
            }
        }

        $edit_link = get_edit_post_link($post_id, 'raw');
        $type_label = ($post_type === 'page') ? __('Page', 'talkgenai') : __('Post', 'talkgenai');

        wp_send_json_success(array(
            'post_id'   => $post_id,
            'edit_link' => $edit_link,
            'message'   => sprintf(
                /* translators: %s: post type label (Post or Page) */
                __('Draft %s created successfully!', 'talkgenai'),
                $type_label
            ),
        ));
    }
}
