<?php
/**
 * Schemafy Admin Class
 *
 * Handles all admin area functionality for Schemafy plugin
 *
 * @package Schemafy
 * @since 1.1.85
 * @author SCHEMAFY
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Class SCHEMAFY_Admin
 * 
 * Manages admin page, scripts, styles, and help tabs
 */
class SCHEMAFY_Admin {

    const META_OPTION_NAME = 'schemafy_custom_meta_tags';
    const META_LOCK_OPTION = 'schemafy_meta_tags_write_lock';
    const META_MAX_TITLE_LENGTH = 200;
    const META_MAX_DESCRIPTION_LENGTH = 500;
    const META_BULK_MAX_ROWS = 500;
    
    /**
     * Validator instance
     *
     * @var SCHEMAFY_Validator
     */
    private $validator;

    /**
     * Database instance
     *
     * @var SCHEMAFY_Database
     */
    private $database;

    /**
     * Brand information
     *
     * @var array
     */
    private $brand = array();

    /**
     * Constructor
     *
     * @param SCHEMAFY_Validator $validator Validator instance
     */
    public function __construct($validator) {
        $this->validator = $validator;
        $this->database = new SCHEMAFY_Database();

        // Initialize brand information
        $this->brand = array(
            'name' => 'Schemafy',
            'plugin_name' => 'Schemafy',
            'plugin_url' => '',
            'author_url' => '',
            'support_url' => '',
            'docs_url' => '',
            'color' => '#5f70de',
            'secondary_color' => '#3730a3'
        );

        // Register Meta Tags AJAX actions
        add_action('wp_ajax_schemafy_get_urls_for_meta', array($this, 'schemafy_get_urls_for_meta'));
        add_action('wp_ajax_schemafy_get_meta_tags', array($this, 'schemafy_get_meta_tags'));
        add_action('wp_ajax_schemafy_save_meta_tags', array($this, 'schemafy_save_meta_tags'));
        add_action('wp_ajax_schemafy_delete_meta_tags', array($this, 'schemafy_delete_meta_tags'));
        add_action('wp_ajax_schemafy_bulk_import_meta_tags', array($this, 'schemafy_bulk_import_meta_tags'));

        // Register Bulk Manager health stats (free, read-only). The paid bulk
        // action tools (Fix Errors, Clean Orphans, Remove Duplicates, Clean
        // Embedded) live in includes/premium/bulk-tools__premium_only.php and
        // register their own AJAX hooks there — they are stripped from the free
        // WordPress.org build entirely.
        add_action('wp_ajax_schemafy_get_bulk_health_stats', array($this, 'schemafy_get_bulk_health_stats'));

        // The AutoGenerator is a paid tool: it registers its own AJAX hooks
        // from includes/premium/auto-generator-handlers__premium_only.php,
        // which is absent from the free package.

        // Plugin Compatibility AJAX handlers
        add_action('wp_ajax_schemafy_save_compatibility', array($this, 'schemafy_save_compatibility'));

        // Add filters to highlight correct submenu based on tab parameter
        add_filter('submenu_file', array($this, 'highlight_submenu_item'));
        add_filter('parent_file', array($this, 'highlight_parent_menu'));
    }

    /**
     * Highlight the correct submenu item based on the tab parameter
     *
     * Returns the relative URL that matches what's stored in the $submenu array
     * so WordPress can highlight the correct menu item.
     *
     * @param string $submenu_file The current submenu file
     * @return string The modified submenu file
     */
    public function highlight_submenu_item($submenu_file) {
        global $pagenow;

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only menu highlighting, no state change
        $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';

        if ($pagenow === 'admin.php' && $page === 'schemafy-custom-schema') {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only menu highlighting, no state change
            $tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'dashboard';

            if ($tab === 'dashboard') {
                return 'schemafy-custom-schema';
            }

            // Return relative URL matching what fix_submenu_urls() stored
            $valid_tabs = array('builder', 'manual', 'ai-paste', 'auto-generator', 'bulk', 'sitemap', 'metadata');
            if (in_array($tab, $valid_tabs)) {
                return 'admin.php?page=schemafy-custom-schema&tab=' . $tab;
            }
        }

        return $submenu_file;
    }

    /**
     * Ensure parent menu stays highlighted for all tabs
     *
     * @param string $parent_file The current parent file
     * @return string The modified parent file
     */
    public function highlight_parent_menu($parent_file) {
        global $pagenow;

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only menu highlighting, no state change
        $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';

        if ($pagenow === 'admin.php' && $page === 'schemafy-custom-schema') {
            return 'schemafy-custom-schema';
        }

        return $parent_file;
    }
    
    /**
     * Add admin menu
     *
     * @return void
     */
    public function add_admin_menu() {
        $page = add_menu_page(
            $this->brand['plugin_name'] . ' - ' . __('Control Panel', 'schemafy'),
            'Schemafy',
            'manage_options',
            'schemafy-custom-schema',
            array($this, 'admin_page'),
            'dashicons-media-code',
            30
        );

        // Define all tabs with their labels
        $tabs = array(
            'dashboard'      => array('icon' => '', 'label' => __('Dashboard', 'schemafy')),
            'builder'        => array('icon' => '', 'label' => __('Smart Builder', 'schemafy')),
            'manual'         => array('icon' => '', 'label' => __('Manual JSON', 'schemafy')),
            'ai-paste'       => array('icon' => '', 'label' => __('AI Generator', 'schemafy')),
            'auto-generator' => array('icon' => '', 'label' => __('AutoGenerator', 'schemafy')),
            'bulk'           => array('icon' => '', 'label' => __('Rich Snippets', 'schemafy')),
            'sitemap'        => array('icon' => '', 'label' => __('Sitemap', 'schemafy')),
            'metadata'       => array('icon' => '', 'label' => __('Meta Tags', 'schemafy')),
        );

        // Register every tab. Premium tabs render an upgrade teaser when the
        // user's plan (or the free build) doesn't include them — they are never
        // hidden by a payment check, which keeps the free build WordPress.org
        // compliant and keeps the upgrade path visible for conversion.
        foreach ($tabs as $tab_slug => $tab_info) {
            $menu_slug = ($tab_slug === 'dashboard')
                ? 'schemafy-custom-schema'
                : 'schemafy-tab-' . $tab_slug;

            add_submenu_page(
                'schemafy-custom-schema',
                $tab_info['label'],
                $tab_info['label'],
                'manage_options',
                $menu_slug,
                array($this, 'admin_page')
            );
        }

        // Rewrite submenu URLs to use ?page=schemafy-custom-schema&tab=X pattern
        add_action('admin_menu', array($this, 'fix_submenu_urls'), 999);

        // Add contextual help
        add_action('load-' . $page, array($this, 'add_help_tabs'));
    }

    /**
     * Fix submenu URLs to use ?page=schemafy-custom-schema&tab=X pattern
     * Uses relative URLs for reliable WordPress menu matching
     */
    public function fix_submenu_urls() {
        global $submenu;

        if (!isset($submenu['schemafy-custom-schema'])) {
            return;
        }

        $tab_mappings = array(
            'schemafy-tab-builder'        => 'builder',
            'schemafy-tab-manual'         => 'manual',
            'schemafy-tab-ai-paste'       => 'ai-paste',
            'schemafy-tab-auto-generator' => 'auto-generator',
            'schemafy-tab-bulk'           => 'bulk',
            'schemafy-tab-sitemap'        => 'sitemap',
            'schemafy-tab-metadata'       => 'metadata',
        );

        foreach ($submenu['schemafy-custom-schema'] as $key => $item) {
            $slug = $item[2];
            if (isset($tab_mappings[$slug])) {
                // Use relative URL — WordPress handles the admin path automatically
                $submenu['schemafy-custom-schema'][$key][2] = 'admin.php?page=schemafy-custom-schema&tab=' . $tab_mappings[$slug];
            }
        }
    }
    
    /**
     * Render admin page
     *
     * @return void
     */
    public function admin_page() {
        // Security check
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'schemafy'));
        }

        // Check if using new interface (v2)
        // Use new enhanced interface
        $template_path = SCHEMAFY_PLUGIN_PATH . 'templates/admin-page-new.php';

        // Make database available to template
        $schemafy_database = $this->database;

        if (file_exists($template_path)) {
            include $template_path;
        } else {
            // Fallback: render basic structure if template doesn't exist
            echo '<div class="wrap">';
            echo '<h1>' . esc_html($this->brand['plugin_name']) . '</h1>';
            echo '<div class="notice notice-warning"><p>';
            echo esc_html__('Template file not found. Please ensure template file exists.', 'schemafy');
            echo '</p></div>';
            echo '<div id="schemafy-schema-app"></div>';
            echo '</div>';
        }
    }
    
    /**
     * Render Schema Builder page
     * 
     * @return void
     */
    public function render_builder_page() {
        // Security check
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'schemafy'));
        }
        
        // Get schema ID if editing
        // Read-only UI state: which schema the builder should open. It changes
        // nothing, so there is no action to protect with a nonce.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reads which tab/row to render, performs no action
        $schema_id = isset($_GET['schema_id']) ? absint($_GET['schema_id']) : 0;
        
        // Access the Schema Builder from the global core instance
        global $schemafy_core_instance;
        if ($schemafy_core_instance && method_exists($schemafy_core_instance, 'get_schema_builder')) {
            $builder = $schemafy_core_instance->get_schema_builder();
            if ($builder) {
                $builder->render_builder(array(
                    'schema_id' => $schema_id,
                    'context' => 'admin'
                ));
            } else {
                echo '<div class="wrap"><h1>Error: Schema Builder not initialized</h1></div>';
            }
        } else {
            echo '<div class="wrap"><h1>Error: Core plugin instance not found</h1></div>';
        }
    }
    
    /**
     * Enqueue admin scripts and styles
     * 
     * @param string $hook Current page hook
     * @return void
     */
    public function enqueue_admin_scripts($hook) {
        // Only load on our plugin page
        if ($hook !== 'toplevel_page_schemafy-custom-schema') {
            return;
        }

        // Enqueue V2 CSS
        wp_enqueue_style(
                'schemafy-schema-admin-v2',
                SCHEMAFY_PLUGIN_URL . 'assets/css/admin-v2.css',
                array(),
                SCHEMAFY_VERSION,
                'all'
            );

            // Enqueue inline styles extracted from templates (WordPress.org compliance)
            wp_enqueue_style(
                'schemafy-inline-styles-admin',
                SCHEMAFY_PLUGIN_URL . 'assets/css/inline-styles-admin.css',
                array('schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                'all'
            );

            // Enqueue Meta Tags & Social Media V2 CSS (Base)
            wp_enqueue_style(
                'schemafy-schema-meta-tags-v2',
                SCHEMAFY_PLUGIN_URL . 'assets/css/admin-meta-tags-v2.css',
                array('schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                'all'
            );

            // Enqueue Modern Meta Tags Design (Override)
            wp_enqueue_style(
                'schemafy-schema-meta-modern',
                SCHEMAFY_PLUGIN_URL . 'assets/css/admin-meta-modern.css',
                array('schemafy-schema-meta-tags-v2'),
                SCHEMAFY_VERSION,
                'all'
            );

        // Inline CSS for tabs and additional styles
        $custom_css = "
        /* Force new tab styles */
        .schemafy-tabs {
            display: flex !important;
            gap: 0 !important;
            border-bottom: 3px solid #e1e5e9 !important;
            margin: 30px 0 40px 0 !important;
            background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%) !important;
            border-radius: 12px 12px 0 0 !important;
            overflow: hidden !important;
            box-shadow: 0 2px 12px rgba(0,0,0,0.08) !important;
            padding: 0 !important;
        }
        
        .schemafy-tab-btn {
            flex: 1 !important;
            padding: 20px 40px !important;
            border: none !important;
            background: transparent !important;
            cursor: pointer !important;
            font-size: 16px !important;
            font-weight: 600 !important;
            color: #6c757d !important;
            border-bottom: 4px solid transparent !important;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
            position: relative !important;
        }
        
        .schemafy-tab-btn:hover {
            color: var(--schemafy-primary) !important;
            background: linear-gradient(180deg, rgba(255, 107, 53, 0.05) 0%, rgba(255, 107, 53, 0.1) 100%) !important;
        }
        
        .schemafy-tab-btn.active {
            color: var(--schemafy-primary) !important;
            border-bottom-color: var(--schemafy-primary) !important;
            background: linear-gradient(180deg, rgba(255, 107, 53, 0.08) 0%, rgba(255, 107, 53, 0.12) 100%) !important;
            font-weight: 700 !important;
        }
        
        .schemafy-tab-content {
            display: none !important;
        }
        
        .schemafy-tab-content.active {
            display: block !important;
        }
        ";
        wp_add_inline_style('schemafy-schema-admin-v2', $custom_css);

        // Enqueue JavaScript files - V2 interface
        // Load utility modules first

        // 1. JSON Validator
            wp_enqueue_script(
                'schemafy-schema-json-validator',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-json-validator.js',
                array('jquery'),
                SCHEMAFY_VERSION,
                true
            );

            // 2. Variables Dropdown
            wp_enqueue_script(
                'schemafy-schema-variables-dropdown',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-variables-dropdown.js',
                array('jquery'),
                SCHEMAFY_VERSION,
                true
            );

            // 3. Bulk Selector
            wp_enqueue_script(
                'schemafy-schema-bulk-selector',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-bulk-selector.js',
                array('jquery'),
                SCHEMAFY_VERSION,
                true
            );

            // 4. Schema Expander
            wp_enqueue_script(
                'schemafy-schema-expander',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-schema-expander.js',
                array('jquery'),
                SCHEMAFY_VERSION,
                true
            );

            // 5. Notifications
            wp_enqueue_script(
                'schemafy-schema-notifications',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-notifications.js',
                array('jquery'),
                SCHEMAFY_VERSION,
                true
            );

            // 6. Main V2 JS - Fully modularized architecture
            if (class_exists('Schemafy_Modular_Scripts')) {
                Schemafy_Modular_Scripts::enqueue_modular_scripts();
            }

            // 6b. Builder Scripts (Smart Builder functionality)
            wp_enqueue_script(
                'schemafy-builder-visual',
                SCHEMAFY_PLUGIN_URL . 'assets/js/builder-visual.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            // JSON Editor (simple textarea-based, no external dependencies)
            wp_enqueue_script(
                'schemafy-builder-json',
                SCHEMAFY_PLUGIN_URL . 'assets/js/builder-json.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            wp_enqueue_script(
                'schemafy-builder-preview',
                SCHEMAFY_PLUGIN_URL . 'assets/js/builder-preview.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            wp_enqueue_script(
                'schemafy-builder-scoring',
                SCHEMAFY_PLUGIN_URL . 'assets/js/builder-scoring.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            wp_enqueue_script(
                'schemafy-builder-app',
                SCHEMAFY_PLUGIN_URL . 'assets/js/builder-app.js',
                array('jquery', 'schemafy-schema-admin-v2', 'schemafy-builder-visual', 'schemafy-builder-json', 'schemafy-builder-preview', 'schemafy-builder-scoring'),
                SCHEMAFY_VERSION,
                true
            );

            // 7. Meta Tags Manager
            // Must depend on schemafy-schema-admin-v2 so schemafy_ajax (nonce, ajax_url)
            // is guaranteed to be defined before admin-meta-tags.js runs.
            // Without this, WooCommerce's admin scripts can shift the load order
            // and cause schemafy_ajax to be undefined → every save silently fails.
            wp_enqueue_script(
                'schemafy-schema-meta-tags',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-meta-tags.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            // 7d. Snippet Editor (Rich Snippet Editor for URL Detail View)
            wp_enqueue_style(
                'schemafy-snippet-editor',
                SCHEMAFY_PLUGIN_URL . 'assets/css/snippet-editor.css',
                array('schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                'all'
            );

            wp_enqueue_script(
                'schemafy-snippet-editor',
                SCHEMAFY_PLUGIN_URL . 'assets/js/snippet-editor.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            // Note: schemafy-snippet-editor depends on schemafy-schema-admin-v2 so schemafy_ajax
            // (including upgrade_url) is already available. A separate localization here
            // would overwrite the full object and strip upgrade_url, breaking upgrade links.

            // Enqueue WordPress media uploader
            wp_enqueue_media();

            // 8. V2 Enhancements Integration (depends on all above)
            wp_enqueue_script(
                'schemafy-schema-admin-v2-enhancements',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-v2-enhancements.js',
                array(
                    'jquery',
                    'schemafy-schema-json-validator',
                    'schemafy-schema-variables-dropdown',
                    'schemafy-schema-bulk-selector',
                    'schemafy-schema-expander',
                    'schemafy-schema-notifications',
                    'schemafy-schema-admin-v2',
                    'schemafy-schema-meta-tags',
                ),
                SCHEMAFY_VERSION,
                true
            );

            // Localize script for V2
            wp_localize_script('schemafy-schema-admin-v2', 'schemafy_ajax', array(
                'ajax_url'        => admin_url('admin-ajax.php'),
                'nonce'           => wp_create_nonce('schemafy_nonce'),
                'site_url'        => get_site_url(),
                'has_woocommerce' => class_exists('WooCommerce'),
                'schema_type_registry' => class_exists('SCHEMAFY_Schema_Type_Registry') ? SCHEMAFY_Schema_Type_Registry::get_client_registry() : array(),
                // Required properties and length limits per schema type, read
                // from the Smart Builder's own field configs so the JSON
                // validators check against the same definition the builder
                // generates from.
                'schema_rules'    => class_exists('SCHEMAFY_Field_Generator') ? SCHEMAFY_Field_Generator::get_validation_rules() : array(),
                'i18n'            => $this->get_i18n_strings(),
                'i18n_scanning'   => __('Scanning your site...', 'schemafy'),
                // Which editors this build can actually open. Presence-first:
                // in the free package the partial is absent, so these are false
                // without any payment check ever running.
                'features'        => array(
                    'ai'     => schemafy_premium_tab_available('ai-paste'),
                    'auto'   => schemafy_premium_tab_available('auto-generator'),
                ),
                'upgrade_url'     => schemafy_upgrade_url(),
            ));

            // Enqueue onboarding tour script
            wp_enqueue_script(
                'schemafy-onboarding-tour',
                SCHEMAFY_PLUGIN_URL . 'assets/js/admin-onboarding-tour.js',
                array('jquery', 'schemafy-schema-admin-v2'),
                SCHEMAFY_VERSION,
                true
            );

            wp_localize_script('schemafy-onboarding-tour', 'SCHEMAFY_TOUR_STRINGS', array(
                'dashboard_title'     => __('Dashboard - Your Control Center', 'schemafy'),
                'dashboard_content'   => __('This is your home base. Here you can see at a glance how many schemas you have created, which pages have them, and the overall status of your site\'s structured data.', 'schemafy'),
                'stats_title'         => __('Your Schema Statistics', 'schemafy'),
                'stats_content'       => __('These cards show you important numbers: how many pages have schemas, total schemas created, how many are active (visible to Google), and how many are paused. Use this to track your progress!', 'schemafy'),
                'map_title'           => __('Schema Map - Your Pages at a Glance', 'schemafy'),
                'map_content'         => __('This list shows all your website pages and what schemas each one has. Click on any URL to see its schemas, edit them, or add new ones. The fastest way to manage schemas by page!', 'schemafy'),
                'builder_title'       => __('Smart Builder - Start Here!', 'schemafy'),
                'builder_content'     => __('This is the EASIEST way to create schemas. No coding needed! Just pick a schema type (like Product, Article, or Business), fill in the blanks, and we create the code for you automatically.', 'schemafy'),
                'types_title'         => __('Pick Your Schema Type', 'schemafy'),
                'types_content'       => __('Each card represents a different type of content. Selling products? Click Product. Writing blog posts? Click Article. Have a local store? Click LocalBusiness. Just pick what matches your page content!', 'schemafy'),
                'manual_title'        => __('Manual JSON - For Advanced Users', 'schemafy'),
                'manual_content'      => __('Already have schema code from another source? Or want full control? Use this to paste or write your own JSON-LD code directly. Great for custom requirements.', 'schemafy'),
                'editor_title'        => __('The JSON Editor', 'schemafy'),
                'editor_content'      => __('Here you select which page the schema goes on, give it a name, and paste your JSON-LD code. Pro tip: You can use variables like {post_title} that automatically fill in with your page data!', 'schemafy'),
                'bulk_title'          => __('Rich Snippets Manager - See Everything', 'schemafy'),
                'bulk_content'        => __('This is your schema library. Every schema you have ever created lives here. You can search, filter, turn them on/off, edit, or delete them. Think of it as your schema filing cabinet!', 'schemafy'),
                'library_title'       => __('Your Complete Schema Library', 'schemafy'),
                'library_content'     => __('Here you see every schema in a table view. Use the search bar to find specific schemas, filter by type or status, and use the action buttons to edit or manage them. You can also export all your schemas as a backup!', 'schemafy'),
                'ai_title'            => __('AI Generator - Automatic Schema Creation', 'schemafy'),
                'ai_content'          => __('Select a page and schema type, and our AI will analyze the content and generate optimized schema markup automatically. Review, edit if needed, and save directly to your site.', 'schemafy'),
                'ai_locked'           => __('Select a page and schema type, and our AI will analyze the content and generate optimized schema markup automatically. Available on the Full plan.', 'schemafy'),
                'finish'              => __('Finish', 'schemafy'),
                'next'                => __('Next', 'schemafy'),
                // Nonces live inside this array (not as standalone scalar
                // localizations) so wp_localize_script emits them as strings —
                // a scalar $l10n would be cast to a JS array and break nonce checks.
                'dismiss_nonce'       => wp_create_nonce('schemafy_dismiss_onboarding'),
                'reset_nonce'         => wp_create_nonce('schemafy_reset_onboarding'),
            ));
    }
    
    /**
     * Get internationalization strings
     * 
     * @return array Array of translatable strings
     */
    private function get_i18n_strings() {
        return array(
            'loading_schemas' => __('Loading schemas...', 'schemafy'),
            'searching' => __('🔍 Searching...', 'schemafy'),
            'no_results' => __('No schemas found', 'schemafy'),
            'empty_state' => __('🌟 Add your first schema!', 'schemafy'),
            'active_short' => __('active', 'schemafy'),
            'updated_label' => __('Updated: ', 'schemafy'),
            'visit' => __('🔗 Visit', 'schemafy'),
            'add_schema' => __('➕ Add Schema', 'schemafy'),
            'edit' => __('✏️ Edit', 'schemafy'),
            'delete' => __('🗑️ Delete', 'schemafy'),
            'activate' => __('▶️ Activate', 'schemafy'),
            'deactivate' => __('⏸️ Deactivate', 'schemafy'),
            'test_on_google' => __('🧪 Test', 'schemafy'),
            'copy_json' => __('📋 Copy', 'schemafy'),
            'save_schema' => __('💾 Save Schema', 'schemafy'),
            'update_schema' => __('🔄 Update Schema', 'schemafy'),
            'cancel' => __('❌ Cancel', 'schemafy'),
            'page_label' => __('Page', 'schemafy'),
            'of_label' => __('of', 'schemafy'),
            'items_label' => __('items', 'schemafy'),
            'prev' => __('← Previous', 'schemafy'),
            'next' => __('Next →', 'schemafy'),
            'validate' => __('✓ Validate', 'schemafy'),
            'valid' => __('✅ Valid', 'schemafy'),
            'error' => __('❌ Error', 'schemafy'),
            'schema_saved' => __('✅ Schema saved successfully!', 'schemafy'),
            'schema_updated' => __('✅ Schema updated successfully!', 'schemafy'),
            'schema_deleted' => __('🗑️ Schema deleted successfully', 'schemafy'),
            'schema_activated' => __('▶️ Schema activated', 'schemafy'),
            'schema_deactivated' => __('⏸️ Schema deactivated', 'schemafy'),
            'error_loading' => __('❌ Error loading schemas', 'schemafy'),
            'error_deleting' => __('❌ Error deleting schema', 'schemafy'),
            'confirm_delete' => __('Are you sure you want to delete this schema?', 'schemafy'),
            'invalid_url' => __('⚠️ Invalid URL', 'schemafy'),
            'invalid_json' => __('❌ Invalid JSON', 'schemafy'),
            'json_valid' => __('✅ JSON valid - Ready to save', 'schemafy'),
            'script_tags_detected' => __('🚨 Script tags detected! Paste only the JSON content starting with { }. Remove <script> and </script> wrapper tags.', 'schemafy'),
            'chars_left' => __('characters left', 'schemafy'),
            'got_it' => __('Got it!', 'schemafy'),

            // Schema CRUD operations
            'loading_schema' => __('Loading schema...', 'schemafy'),
            'network_error_loading' => __('Network error loading schema', 'schemafy'),
            'smart_builder_tab_not_found' => __('Smart Builder tab not found. Opening in Manual JSON.', 'schemafy'),
            'error_parsing_schema' => __('Error parsing schema data. Opening in Manual JSON.', 'schemafy'),
            'invalid_schema_format' => __('Invalid schema format. Opening in Manual JSON.', 'schemafy'),
            'schema_type_not_found' => __('Schema type not found. Opening in Manual JSON.', 'schemafy'),
            'form_not_found' => __('Form not found', 'schemafy'),
            'schema_loaded_smart_builder' => __('Schema loaded in Smart Builder for editing', 'schemafy'),
            'schema_loaded_manual_json' => __('Schema loaded in Manual JSON for editing', 'schemafy'),
            'edit_cancelled' => __('Edit cancelled', 'schemafy'),
            'manual_json_tab_not_found' => __('Manual JSON tab not found', 'schemafy'),
            'schema_status_toggled' => __('Schema status toggled', 'schemafy'),
            'error_toggling' => __('Error toggling schema', 'schemafy'),
            'network_error_toggling' => __('Network error toggling schema', 'schemafy'),
            'network_error_deleting' => __('Network error while deleting schema', 'schemafy'),
            'duplicating_schema' => __('Duplicating schema...', 'schemafy'),
            'schema_duplicated' => __('Schema duplicated successfully', 'schemafy'),
            'error_duplicating' => __('Error duplicating schema', 'schemafy'),
            'network_error' => __('Network error', 'schemafy'),
            'error_saving' => __('Error saving schema', 'schemafy'),
            'error_saving_schemas' => __('Error saving schemas', 'schemafy'),
            'network_error_saving' => __('Network error: Could not save schema', 'schemafy'),
            'network_error_saving_schemas' => __('Network error: Could not save schemas', 'schemafy'),
            'status_active' => __('Active', 'schemafy'),
            'status_inactive' => __('Inactive', 'schemafy'),
            'operation_cancelled' => __('Operation cancelled', 'schemafy'),
            'error_deleting_existing' => __('Error deleting existing schema', 'schemafy'),
            'invalid_schema_id' => __('Invalid schema ID', 'schemafy'),
            'invalid_schema_data' => __('Invalid schema data', 'schemafy'),

            // Sitemap autosave UI
            'sitemap_changes_pending' => __('Changes pending', 'schemafy'),
            'sitemap_select_source_before_enable' => __('Select at least one content source before enabling the sitemap.', 'schemafy'),
            'sitemap_select_source_short' => __('Select a source', 'schemafy'),
            'sitemap_changes_queued' => __('Changes queued', 'schemafy'),
            'sitemap_already_current' => __('Already current', 'schemafy'),
            'sitemap_saved_short' => __('Saved', 'schemafy'),
            'sitemap_retrying' => __('Retrying...', 'schemafy'),
            'sitemap_status_refreshed_short' => __('Status refreshed', 'schemafy'),
            'sitemap_status_refreshed' => __('Sitemap status refreshed.', 'schemafy'),
            'sitemap_refresh_failed' => __('Refresh failed', 'schemafy'),
            'sitemap_error_refreshing_status' => __('Error refreshing sitemap status.', 'schemafy'),
            'sitemap_network_error_refreshing_status' => __('Network error while refreshing sitemap status.', 'schemafy'),
            'sitemap_activate' => __('Activate sitemap', 'schemafy'),
            'sitemap_deactivate' => __('Deactivate sitemap', 'schemafy'),
            'sitemap_autosaves_immediately' => __('Autosaves immediately', 'schemafy'),
            'sitemap_ready' => __('Ready', 'schemafy'),
            'sitemap_active_conflicts_disabled' => __('Schemafy sitemap is active. Duplicate sitemap modules were disabled.', 'schemafy'),
            'sitemap_settings_saved' => __('Sitemap settings saved.', 'schemafy'),
            'sitemap_error_saving_settings' => __('Error saving sitemap settings.', 'schemafy'),
            'sitemap_save_failed' => __('Save failed', 'schemafy'),
            'sitemap_network_error_saving_settings' => __('Network error while saving sitemap settings.', 'schemafy'),
            'sitemap_no_duplicate_generators' => __('No duplicate sitemap generators detected', 'schemafy'),
            'sitemap_no_duplicate_protection' => __('Schemafy will still protect the sitemap index URL when active.', 'schemafy'),
            'sitemap_url_copied' => __('Sitemap URL copied.', 'schemafy'),
            'sitemap_url_copy_failed' => __('Could not copy sitemap URL.', 'schemafy'),
            'clean_label' => __('Clean', 'schemafy'),
            'detected_label' => __('Detected', 'schemafy'),

            // Smart Builder - form structure labels
            'loading_fields'           => __('Loading fields...', 'schemafy'),
            'error_loading_fields'     => __('Error loading fields', 'schemafy'),
            'target_urls_label'        => __('Target URLs', 'schemafy'),
            'select_url_placeholder'   => __('Select a URL...', 'schemafy'),
            'schema_name_label'        => __('Schema Name', 'schemafy'),
            'fields_heading'           => __('Fields', 'schemafy'),
            'loading_urls'             => __('Loading URLs...', 'schemafy'),
            'failed_load_urls'         => __('Failed to load URLs', 'schemafy'),

            // Smart Builder - field rendering
            'variables_btn'            => __('Variables', 'schemafy'),
            'select_placeholder'       => __('Select...', 'schemafy'),
            'select_or_enter_image'    => __('Select or enter image URL', 'schemafy'),
            'insert_variable_title'    => __('Insert Variable', 'schemafy'),
            'click_to_select_date'     => __('Click to select date using the date picker', 'schemafy'),
            'add_another'              => __('Add Another', 'schemafy'),

            // Smart Builder - variable dropdown
            'insert_dynamic_variable'  => __('Insert Dynamic Variable', 'schemafy'),
            'click_variable_to_insert' => __('Click a variable to insert it at cursor position', 'schemafy'),
            'variable_inserted'        => __('Variable inserted!', 'schemafy'),

            // Smart Builder - notifications
            'image_selected' => __('Image selected successfully', 'schemafy'),
            'use_date_picker' => __('Please use the date picker to select a date', 'schemafy'),
            'numbers_use_dot' => __('Use digits only — for decimals use a dot (e.g. 4.5), no commas or thousands separators.', 'schemafy'),

            // Smart Builder - field widgets
            'phone_format_hint' => __('International format recommended: +57 601 1234567', 'schemafy'),
            'duration_widget_help' => __('Set hours and minutes — the format Google requires is generated automatically.', 'schemafy'),
            'hours_label' => __('Hours', 'schemafy'),
            'minutes_label' => __('Minutes', 'schemafy'),
            'hours_short' => __('h', 'schemafy'),
            'minutes_short' => __('min', 'schemafy'),
            'select_currency' => __('— Select currency —', 'schemafy'),

            // Smart Builder - pre-save validation
            'val_fix_before_save' => __('Fix these issues before saving:', 'schemafy'),
            'val_url_absolute' => __('must be an absolute URL starting with https://', 'schemafy'),
            'val_unknown_variable' => __('is not a known variable — it would be published literally. Check the spelling.', 'schemafy'),
            'val_rating_range' => __('Rating value', 'schemafy'),
            'val_rating_range_2' => __('is outside the allowed range', 'schemafy'),
            'val_review_count' => __('Review count must be at least 1', 'schemafy'),
            'val_offer_currency' => __('The offer has a price but no currency — Google requires both together.', 'schemafy'),
            'val_offer_price' => __('The offer is missing its price — Google requires price and currency together.', 'schemafy'),
            'val_currency_iso' => __('is not a valid ISO 4217 currency code (use USD, EUR, COP…)', 'schemafy'),
            'val_time_format' => __('must use 24h HH:MM format (e.g. 09:00, 18:30)', 'schemafy'),
            'val_duration_format' => __('is not a valid ISO 8601 duration (expected e.g. PT30M or PT1H30M)', 'schemafy'),
            'val_gtin_format' => __('must be 8, 12, 13 or 14 digits with no spaces or dashes', 'schemafy'),
            'val_phone_e164' => __('Phone numbers work best in international format (+57 601 1234567)', 'schemafy'),
            'val_date_order' => __('cannot be earlier than', 'schemafy'),
            'val_requires' => __('requires', 'schemafy'),
            'val_item_reviewed' => __('what is being reviewed', 'schemafy'),
            'val_faq_questions' => __('at least one question and answer', 'schemafy'),
            'val_product_needs' => __('A Product needs at least one of: offers, review or aggregateRating.', 'schemafy'),
            'val_review_host' => __('does not support rating/review snippets in Google. Use LocalBusiness or Product for ratings.', 'schemafy'),
            'val_worst_negative' => __('worstRating cannot be negative (use 0 or 1).', 'schemafy'),
            'val_best_worst' => __('bestRating must be greater than worstRating.', 'schemafy'),
            'val_video_field' => __('The video is missing the required field', 'schemafy'),
            'val_video_url' => __('The video needs a contentUrl or embedUrl.', 'schemafy'),
            'val_dotted_key' => __('is a malformed property (the nested object did not expand). Re-open and re-save this field.', 'schemafy'),
            'only_one_url' => __('Only one URL allowed when editing a schema', 'schemafy'),
            'url_already_added' => __('This URL is already added', 'schemafy'),
            'cannot_remove_url' => __('Cannot remove URL while editing a schema', 'schemafy'),
            'json_copied' => __('JSON copied to clipboard!', 'schemafy'),
            'failed_to_copy' => __('Failed to copy', 'schemafy'),
            'fill_schema_name' => __('Please fill Schema Name', 'schemafy'),
            'add_target_url' => __('Please add a Target URL', 'schemafy'),
            'add_one_target_url' => __('Please add at least one Target URL', 'schemafy'),
            'error_updating' => __('Error updating schema', 'schemafy'),
            'network_error_updating' => __('Network error: Could not update schema', 'schemafy'),

            // Meta Tags
            'empty_will_use_default' => __('Empty (will use default)', 'schemafy'),
            'perfect_length' => __('Perfect length!', 'schemafy'),
            'too_short_title' => __('Too short (aim for 50-60)', 'schemafy'),
            'too_short_desc' => __('Too short (aim for 120-160)', 'schemafy'),
            'a_bit_long' => __('A bit long', 'schemafy'),
            'too_long_cutoff' => __('Too long! Will be cut off', 'schemafy'),
            'error_loading_meta' => __('Error loading meta tags', 'schemafy'),
            'please_select_page' => __('Please select a page first', 'schemafy'),
            'please_enter_title_or_desc' => __('Please enter at least a title or description', 'schemafy'),
            'saving' => __('Saving...', 'schemafy'),
            'meta_saved' => __('Meta tags saved successfully!', 'schemafy'),
            'permission_denied_refresh' => __('Permission denied. Please refresh the page and try again.', 'schemafy'),
            'network_error_connection' => __('Network error. Please check your connection.', 'schemafy'),
            'no_custom_meta' => __('No custom meta tags to clear', 'schemafy'),
            'meta_cleared' => __('Meta tags cleared successfully', 'schemafy'),
            'error_saving_meta' => __('Error saving meta tags', 'schemafy'),
            'error_clearing_meta' => __('Error clearing meta tags', 'schemafy'),
            'loading_pages' => __('Loading pages...', 'schemafy'),
            'pages_label' => __('Pages', 'schemafy'),
            'posts_label' => __('Posts', 'schemafy'),
            'products_label' => __('Products', 'schemafy'),
            'error_loading_pages' => __('Error loading pages', 'schemafy'),
            'no_pages_found' => __('No pages found', 'schemafy'),
            'select_page_placeholder' => __('Select a page', 'schemafy'),
            'some_meta_sources_failed' => __('Some content sources could not be loaded.', 'schemafy'),
            'detecting_seo_plugin' => __('Detecting SEO plugin...', 'schemafy'),
            'no_title_detected' => __('No title detected', 'schemafy'),
            'no_description_detected' => __('No description detected', 'schemafy'),
            'wordpress_default' => __('WordPress Default', 'schemafy'),
            'serp_title_placeholder' => __('Your Page Title Will Appear Here', 'schemafy'),
            'serp_description_placeholder' => __('Your meta description will appear here. Make it compelling to improve click-through rates from search results.', 'schemafy'),
            'meta_save_in_progress' => __('Schemafy is already saving these meta tags. Please wait a moment.', 'schemafy'),
            'meta_clear_in_progress' => __('Schemafy is already clearing these meta tags. Please wait a moment.', 'schemafy'),
            'meta_title_too_long_max' => __('Meta title is too long. Maximum length is {max} characters.', 'schemafy'),
            'meta_description_too_long_max' => __('Meta description is too long. Maximum length is {max} characters.', 'schemafy'),
            'confirm_clear_meta' => __('Are you sure you want to clear the custom meta tags and revert to the default SEO plugin values?', 'schemafy'),
            'clearing' => __('Clearing...', 'schemafy'),
            'unknown_error' => __('Unknown error', 'schemafy'),
            'meta_busy_retry' => __('Schemafy is already saving meta tags. Please retry in a moment.', 'schemafy'),

            // JSON Validator & v2 enhancements
            'valid_badge' => __('Valid', 'schemafy'),
            'invalid_badge' => __('Invalid', 'schemafy'),
            'valid_json_ready' => __('✓ Valid JSON - Ready to save', 'schemafy'),
            'invalid_json_prefix' => __('✗ Invalid JSON: ', 'schemafy'),
            'valid_json_indicator' => __('✓ Valid JSON', 'schemafy'),
            'invalid_json_indicator' => __('✗ Invalid JSON', 'schemafy'),
            'json_formatted_success' => __('JSON formatted successfully!', 'schemafy'),
            'cannot_format_json' => __('Cannot format invalid JSON', 'schemafy'),
            'json_is_valid' => __('JSON is valid!', 'schemafy'),
            'json_validation_failed' => __('JSON validation failed', 'schemafy'),
            'json_formatted_short' => __('JSON formatted!', 'schemafy'),
            'json_is_valid_check' => __('✓ JSON is valid!', 'schemafy'),
            'json_validation_failed_short' => __('✗ JSON validation failed', 'schemafy'),
            'schemas_expanded' => __('All schemas expanded', 'schemafy'),
            'schemas_collapsed' => __('All schemas collapsed', 'schemafy'),
            'error_occurred_saving' => __('An error occurred while saving', 'schemafy'),
            'yes_delete' => __('Yes, Delete', 'schemafy'),

            // Notifications dialog
            'confirm_label' => __('Confirm', 'schemafy'),
            'cancel_label' => __('Cancel', 'schemafy'),
            'loading_label' => __('Loading...', 'schemafy'),

            // Builder Visual
            'upload_image' => __('Upload Image', 'schemafy'),
            'select_image_title' => __('Select Image', 'schemafy'),
            'use_this_image' => __('Use this image', 'schemafy'),
            'add_item' => __('+ Add Item', 'schemafy'),
            'remove_item' => __('Remove', 'schemafy'),

            // Manual Editor
            'json_formatted_manual' => __('JSON formatted', 'schemafy'),
            'invalid_json_cannot_format' => __('Invalid JSON - cannot format', 'schemafy'),
            'json_minified' => __('JSON minified', 'schemafy'),
            'invalid_json_cannot_minify' => __('Invalid JSON - cannot minify', 'schemafy'),
            'failed_copy_clipboard' => __('Failed to copy to clipboard', 'schemafy'),
            'editor_cleared' => __('Editor cleared', 'schemafy'),
            'invalid_json_format' => __('Invalid JSON format', 'schemafy'),
            'error_updating_schema' => __('Error updating schema', 'schemafy'),
            'error_saving_schema' => __('Error saving schema', 'schemafy'),

            // Duplicate schema modal
            'duplicate_detected_title' => __('Duplicate Schema Detected', 'schemafy'),
            // translators: %s is a value inserted at runtime.
            'duplicate_exists_msg' => __('A schema of type "%s" already exists for this URL:', 'schemafy'),
            'name_label' => __('Name:', 'schemafy'),
            'created_label' => __('Created:', 'schemafy'),
            'status_label' => __('Status:', 'schemafy'),
            'seo_recommendation_label' => __('SEO Recommendation:', 'schemafy'),
            'duplicate_seo_warning' => __('Having multiple schemas of the same type for a URL can confuse search engines and negatively affect your SEO.', 'schemafy'),
            'what_to_do' => __('What would you like to do?', 'schemafy'),
            'replace_recommended' => __('Replace (Recommended)', 'schemafy'),
            'add_anyway' => __('Add Anyway', 'schemafy'),
            'replacing' => __('Replacing...', 'schemafy'),
            'form_loading_timeout' => __('Form loading timeout. Opening in Manual JSON instead.', 'schemafy'),

            // Edit mode selection modal
            'edit_schema_prefix' => __('Edit Schema: ', 'schemafy'),
            'choose_edit_mode' => __('Choose how you want to edit this schema:', 'schemafy'),
            'use_smart_builder' => __('Use Smart Builder', 'schemafy'),
            'visual_guided_fields' => __('Visual interface with guided fields', 'schemafy'),
            'no_coding_required' => __('No coding required', 'schemafy'),
            'pre_configured_templates' => __('Pre-configured templates', 'schemafy'),
            'field_validation_label' => __('Field validation', 'schemafy'),
            'beginner_friendly' => __('Beginner friendly', 'schemafy'),
            'use_manual_json' => __('Use Manual JSON', 'schemafy'),
            'direct_json_editor' => __('Direct JSON code editor', 'schemafy'),
            'full_control' => __('Full control', 'schemafy'),
            'custom_properties' => __('Custom properties', 'schemafy'),
            'advanced_schemas_label' => __('Advanced schemas', 'schemafy'),
            'for_developers' => __('For developers', 'schemafy'),
            'recommended_badge' => __('Recommended', 'schemafy'),

            // Save success placeholder
            'schema_saved_title' => __('Schema Saved Successfully!', 'schemafy'),
            'select_another_type' => __('Select another type to create a new schema', 'schemafy'),

            // Manual JSON editing requires upgrade
            'not_in_your_plan' => __('Not included in your plan', 'schemafy'),
            'see_plans' => __('See plans', 'schemafy')
        );
    }
    
    /**
     * Add help tabs to admin page
     * 
     * @return void
     */
    public function add_help_tabs() {
        $screen = get_current_screen();
        
        $screen->add_help_tab(array(
            'id' => 'schemafy-schema-overview',
            'title' => '📋 ' . __('Overview', 'schemafy'),
            'content' => $this->get_help_content_overview()
        ));
        
        $screen->add_help_tab(array(
            'id' => 'schemafy-schema-usage',
            'title' => '🎯 ' . __('How to Use', 'schemafy'),
            'content' => $this->get_help_content_usage()
        ));
        
        $screen->set_help_sidebar($this->get_help_sidebar());
    }
    
    /**
     * Get overview help content
     * 
     * @return string HTML content
     */
    private function get_help_content_overview() {
        return '<h4>🎯 ' . __('What does this plugin do?', 'schemafy') . '</h4>
                <p><strong>' . esc_html($this->brand['plugin_name']) . '</strong> ' . __('allows you to add custom structured data (Schema.org) to any page of your website to improve your search engine rankings.', 'schemafy') . '</p>
                <h4>✨ ' . __('Benefits:', 'schemafy') . '</h4>
                <ul>
                    <li>🚀 <strong>' . __('Better SEO:', 'schemafy') . '</strong> ' . __('Rich snippets in Google', 'schemafy') . '</li>
                    <li>🎨 <strong>' . __('Easy to use:', 'schemafy') . '</strong> ' . __('Intuitive interface', 'schemafy') . '</li>
                    <li>⚡ <strong>' . __('Fast:', 'schemafy') . '</strong> ' . __('Optimized for performance', 'schemafy') . '</li>
                    <li>🔒 <strong>' . __('Secure:', 'schemafy') . '</strong> ' . __('Integrated JSON validation', 'schemafy') . '</li>
                </ul>';
    }
    
    /**
     * Get usage help content
     * 
     * @return string HTML content
     */
    private function get_help_content_usage() {
        return '<h4>📋 ' . __('Steps to add a schema:', 'schemafy') . '</h4>
                <ol>
                    <li>' . __('Enter the <strong>complete URL</strong> of the page', 'schemafy') . '</li>
                    <li>' . __('Select the appropriate <strong>schema type</strong>', 'schemafy') . '</li>
                    <li>' . __('Paste your <strong>JSON-LD</strong> code (without script tags)', 'schemafy') . '</li>
                    <li>' . __('Click <strong>"Validate"</strong> to verify syntax', 'schemafy') . '</li>
                    <li>' . __('Save and <strong>you\'re done!</strong>', 'schemafy') . '</li>
                </ol>';
    }
    
    /**
     * Get help sidebar content
     * 
     * @return string HTML content
     */
    private function get_help_sidebar() {
        $sidebar = '<p><strong>🎯 ' . esc_html($this->brand['plugin_name']) . '</strong></p>
                <p>' . sprintf(
                    /* translators: %s is the developer/brand name. */
                    __('Developed by %s', 'schemafy'),
                    esc_html($this->brand['name'])
                ) . ' &amp; <a href="https://agenciawebtek.com" target="_blank" rel="noopener noreferrer">WebTek</a></p>';

        return $sidebar;
    }

    /**
     * Add custom links in plugins page
     * 
     * @param array $links Existing links
     * @return array Modified links
     */
    public function add_plugin_links($links) {
        $custom_links = array(
            '<a href="' . esc_url(admin_url('admin.php?page=schemafy-custom-schema')) . '" style="color: ' . esc_attr($this->brand['color']) . '; font-weight: bold;">' . esc_html__('Configure', 'schemafy') . '</a>'
        );
        return array_merge($custom_links, $links);
    }
    
    /**
     * Add custom metadata in plugins page
     * 
     * @param array $plugin_meta Existing meta
     * @param string $plugin_file Plugin file
     * @return array Modified meta
     */
    public function add_plugin_meta($plugin_meta, $plugin_file) {
        if ($plugin_file === SCHEMAFY_PLUGIN_BASENAME) {
            // translators: %s is a value inserted at runtime.
            $plugin_meta[] = '<span style="color: #666;">' . sprintf(esc_html__('Made with ❤️ by %s', 'schemafy'), esc_html($this->brand['name'])) . ' &amp; <a href="https://agenciawebtek.com" target="_blank" rel="noopener noreferrer">WebTek</a></span>';
        }
        return $plugin_meta;
    }
    
    /**
     * Add button to admin bar
     * 
     * @param WP_Admin_Bar $wp_admin_bar Admin bar object
     * @return void
     */
    public function add_admin_bar_button($wp_admin_bar) {
        if (!current_user_can('manage_options')) {
            return;
        }
        
        $wp_admin_bar->add_node(array(
            'id' => 'schemafy-custom-schema',
            'title' => '🚀 ' . __('Schemafy', 'schemafy'),
            'href' => admin_url('admin.php?page=schemafy-custom-schema'),
            'meta' => array(
                'class' => 'schemafy-admin-bar-button'
            )
        ));
    }
    
    /**
     * Show activation notice
     * 
     * @return void
     */
    public function show_activation_notice() {
        if (!get_transient('schemafy_schema_activation_notice')) {
            return;
        }
        
        ?>
        <div class="notice notice-success is-dismissible">
            <p>
                <strong>🚀 <?php echo esc_html($this->brand['plugin_name']); ?></strong> 
                <?php esc_html_e('has been activated successfully!', 'schemafy'); ?>
                <a href="<?php echo esc_url( admin_url('admin.php?page=schemafy-custom-schema') ); ?>" class="button button-primary" style="margin-left: 10px;">
                    <?php esc_html_e('Get Started', 'schemafy'); ?>
                </a>
            </p>
        </div>
        <?php
        
        delete_transient('schemafy_schema_activation_notice');
    }

    /**
     * AJAX Handler: Get URLs for meta tags management
     */
    public function schemafy_get_urls_for_meta() {
        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Unauthorized', 'schemafy')), 401);
        }

        // Verify nonce
        $nonce = sanitize_text_field($this->get_meta_request_string('nonce'));
        if (!$nonce || !wp_verify_nonce($nonce, 'schemafy_nonce')) {
            wp_send_json_error(array('message' => __('Invalid nonce', 'schemafy')), 403);
        }

        $post_type = $this->get_meta_request_string('post_type');
        $post_type = $post_type !== '' ? sanitize_key($post_type) : 'page';
        $results = array();

        if (!post_type_exists($post_type)) {
            wp_send_json_success($results);
        }

        $post_type_object = get_post_type_object($post_type);
        if (!$post_type_object || empty($post_type_object->public)) {
            wp_send_json_error(array('message' => __('Unsupported post type.', 'schemafy')), 400);
        }

        // Always prepend the homepage as the first option when fetching pages
        if ( $post_type === 'page' ) {
            $front_page_id = (int) get_option( 'page_on_front' );
            $results[] = array(
                'id'    => $front_page_id > 0 ? $front_page_id : 0,
                'title' => __( 'Homepage', 'schemafy' ),
                'url'   => trailingslashit( home_url( '/' ) ),
                'is_homepage' => true,
            );
        }

        $posts = get_posts(array(
            'post_type' => $post_type,
            'posts_per_page' => -1,
            'post_status' => 'publish',
            'orderby' => 'title',
            'order' => 'ASC',
            'no_found_rows' => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ));

        // Exclude the static front page from the list if already added above
        $front_page_id = (int) get_option( 'page_on_front' );
        foreach ($posts as $post) {
            if ( $post_type === 'page' && $front_page_id > 0 && $post->ID === $front_page_id ) {
                continue; // already listed as Homepage
            }
            $results[] = array(
                'id' => $post->ID,
                'title' => $post->post_title,
                'url' => get_permalink($post->ID)
            );
        }

        wp_send_json_success($results);
    }

    /**
     * AJAX Handler: Get current meta tags for a URL
     */
    public function schemafy_get_meta_tags() {
        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Unauthorized', 'schemafy')), 401);
        }

        // Verify nonce
        $nonce = sanitize_text_field($this->get_meta_request_string('nonce'));
        if (!$nonce || !wp_verify_nonce($nonce, 'schemafy_nonce')) {
            wp_send_json_error(array('message' => __('Invalid nonce', 'schemafy')), 403);
        }

        $url = $this->normalize_meta_url($this->get_meta_request_string('url'));
        $post_id = $this->get_meta_request_absint('post_id');

        if (!$url) {
            wp_send_json_error(array('message' => __('A valid URL is required.', 'schemafy')), 400);
        }

        if (!$this->is_same_site_meta_url($url)) {
            wp_send_json_error(array('message' => __('Only URLs from this WordPress site can be managed here.', 'schemafy')), 400);
        }

        $post_id = $this->resolve_meta_post_id($url, $post_id);

        global $schemafy_core_instance;
        $seo_detector = $schemafy_core_instance->get_seo_detector();

        // Get current meta tags from SEO plugins or WordPress
        $current_title = $seo_detector->get_meta_title($post_id, $url);
        $current_description = $seo_detector->get_meta_description($post_id, $url);

        // Get the editable value: post meta when the URL is a post, so an edit
        // made in the active SEO plugin's own panel shows up here too.
        $custom_meta = $this->get_effective_custom_meta($url, $post_id);

        // Determine sources
        $title_source = $this->determine_meta_source($post_id, 'title', $url);
        $description_source = $this->determine_meta_source($post_id, 'description', $url);

        wp_send_json_success(array(
            'url' => $url,
            'post_id' => $post_id,
            'current_title' => $current_title,
            'current_description' => $current_description,
            'custom_title' => $custom_meta['title'],
            'custom_description' => $custom_meta['description'],
            'title_source' => $title_source,
            'description_source' => $description_source
        ));
    }

    /**
     * AJAX Handler: Save meta tags
     */
    public function schemafy_save_meta_tags() {
        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Unauthorized', 'schemafy')), 401);
        }

        // Verify nonce
        $nonce = sanitize_text_field($this->get_meta_request_string('nonce'));
        if (!$nonce || !wp_verify_nonce($nonce, 'schemafy_nonce')) {
            wp_send_json_error(array('message' => __('Invalid nonce', 'schemafy')), 403);
        }

        $url = $this->normalize_meta_url($this->get_meta_request_string('url'));
        $post_id = $this->get_meta_request_absint('post_id');
        $meta_title = sanitize_text_field($this->get_meta_request_string('meta_title'));
        $meta_description = sanitize_textarea_field($this->get_meta_request_string('meta_description'));

        if (!$url) {
            wp_send_json_error(array('message' => __('A valid URL is required.', 'schemafy')), 400);
        }

        if (!$this->is_same_site_meta_url($url)) {
            wp_send_json_error(array('message' => __('Only URLs from this WordPress site can be managed here.', 'schemafy')), 400);
        }

        if ($meta_title === '' && $meta_description === '') {
            wp_send_json_error(array('message' => __('Enter a meta title or description before saving.', 'schemafy')), 400);
        }

        $length_error = $this->validate_meta_lengths($meta_title, $meta_description);
        if ($length_error) {
            wp_send_json_error(array('message' => $length_error), 400);
        }

        $post_id = $this->resolve_meta_post_id($url, $post_id);
        $lock_token = $this->acquire_meta_lock();
        if (!$lock_token) {
            wp_send_json_error(array('message' => __('Schemafy is already saving meta tags. Please retry in a moment.', 'schemafy')), 423);
        }

        $saved = false;
        $response = array();

        try {
            $meta_tags = get_option(self::META_OPTION_NAME, array());
            if (!is_array($meta_tags)) {
                $meta_tags = array();
            }

            $existing_key = $this->find_custom_meta_key($meta_tags, $url);
            if ($existing_key && $existing_key !== $url) {
                unset($meta_tags[$existing_key]);
            }

            $meta_tags[$url] = array(
                'title'       => $meta_title,
                'description' => $meta_description,
                'post_id'     => $post_id,
                'updated_at'  => current_time('mysql'),
            );
            $saved = update_option(self::META_OPTION_NAME, $meta_tags, false);

            if (!$saved) {
                $verify = get_option(self::META_OPTION_NAME, array());
                $saved = is_array($verify)
                    && isset($verify[$url])
                    && (string) $verify[$url]['title'] === (string) $meta_title
                    && (string) $verify[$url]['description'] === (string) $meta_description;
            }

            if ($saved) {
                // Save to post meta only after the canonical option write is confirmed.
                if ($post_id > 0) {
                    // Writes wherever these fields actually live on this site —
                    // the active SEO plugin's rows when one owns them.
                    SCHEMAFY_Meta_Store::set($post_id, $meta_title, $meta_description);
                }

                $response = array(
                    'message' => __('Meta tags saved successfully', 'schemafy'),
                    'url' => $url,
                    'post_id' => $post_id,
                    'custom_title' => $meta_title,
                    'custom_description' => $meta_description,
                );
            }
        } catch (\Throwable $exception) {
            $saved = false;
        }

        $this->release_meta_lock($lock_token);

        if (!$saved) {
            wp_send_json_error(array('message' => __('Failed to save meta tags to database. Please try again.', 'schemafy')), 500);
        }

        wp_send_json_success($response);
    }

    /**
     * AJAX Handler: Bulk import meta tags from CSV rows
     */
    public function schemafy_bulk_import_meta_tags() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Unauthorized', 'schemafy' ) ), 401 );
        }
        $nonce = sanitize_text_field($this->get_meta_request_string('nonce'));
        if ( ! $nonce || ! wp_verify_nonce( $nonce, 'schemafy_nonce' ) ) {
            wp_send_json_error( array( 'message' => __( 'Invalid nonce', 'schemafy' ) ), 403 );
        }

        // Extend resource limits for large imports, best-effort: hosts routinely
        // disable set_time_limit(), and a disabled function emits a warning we
        // must not surface to the user mid-import. The client-side AJAX timeout
        // is 120s to match.
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( 120 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_set_time_limit -- may be disabled by the host; the import is a single long request
        }

        // Memory goes through the WordPress API rather than ini_set(): it
        // respects WP_MAX_MEMORY_LIMIT, is filterable by the host, and never
        // lowers a limit that is already higher.
        wp_raise_memory_limit( 'admin' );

        // Decode and validate payload
        $raw = $this->get_meta_request_string('rows');
        if ( empty( $raw ) ) {
            wp_send_json_error( array( 'message' => __( 'No data received.', 'schemafy' ) ) );
        }

        // json_decode() validates shape, never content. Every field taken from
        // a row below is sanitized with the function that fits it —
        // sanitize_text_field() for titles, sanitize_textarea_field() for
        // descriptions (newlines are meaningful there), esc_url_raw()/
        // normalize_meta_url() for URLs — so no raw value reaches storage.
        $rows = json_decode( $raw, true );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            wp_send_json_error( array(
                'message' => sprintf(
                    /* translators: %s: JSON parser error message. */
                    __( 'Invalid data format: %s', 'schemafy' ),
                    json_last_error_msg()
                ),
            ) );
        }
        if ( ! is_array( $rows ) || empty( $rows ) ) {
            wp_send_json_error( array( 'message' => __( 'No rows provided.', 'schemafy' ) ) );
        }

        // Hard cap to prevent runaway imports
        $max_rows = self::META_BULK_MAX_ROWS;
        if ( count( $rows ) > $max_rows ) {
            wp_send_json_error( array(
                'message' => sprintf(
                    /* translators: 1: submitted rows count, 2: maximum allowed rows. */
                    __( 'Too many rows (%1$d). Maximum per batch is %2$d. Please split your CSV into smaller files.', 'schemafy' ),
                    count( $rows ),
                    $max_rows
                ),
            ) );
        }

        $lock_token = $this->acquire_meta_lock(180);
        if (!$lock_token) {
            wp_send_json_error(array('message' => __('Schemafy is already saving meta tags. Please retry in a moment.', 'schemafy')), 423);
        }

        // Load existing stored meta tags
        $meta_tags = get_option( self::META_OPTION_NAME, array() );
        if ( ! is_array( $meta_tags ) ) {
            $meta_tags = array();
        }

        // Detect site scheme once for URL normalization
        $site_scheme = wp_parse_url( get_site_url(), PHP_URL_SCHEME );

        $saved   = 0;
        $updated = 0;
        $skipped = 0;
        $results = array();
        $expected_meta_tags = array();
        $post_meta_updates = array();

        foreach ( $rows as $row ) {
            if ( ! is_array( $row ) ) {
                $results[] = array( 'status' => 'skipped', 'reason' => __( 'Invalid row format', 'schemafy' ) );
                $skipped++;
                continue;
            }

            $raw_url = isset( $row['url'] ) && is_scalar( $row['url'] ) ? trim( wp_unslash( (string) $row['url'] ) ) : '';
            $title   = isset( $row['meta_title'] ) && is_scalar( $row['meta_title'] ) ? sanitize_text_field( trim( wp_unslash( (string) $row['meta_title'] ) ) ) : '';
            $desc    = isset( $row['meta_description'] ) && is_scalar( $row['meta_description'] ) ? sanitize_textarea_field( trim( wp_unslash( (string) $row['meta_description'] ) ) ) : '';

            // Skip rows with no URL
            if ( ! $raw_url ) {
                $results[] = array( 'status' => 'skipped', 'reason' => __( 'Empty URL', 'schemafy' ) );
                $skipped++;
                continue;
            }

            // Auto-upgrade HTTP → HTTPS when the site runs on HTTPS
            if ( 'https' === $site_scheme && strpos( $raw_url, 'http://' ) === 0 ) {
                $raw_url = 'https://' . substr( $raw_url, 7 );
            }

            $url = $this->normalize_meta_url( $raw_url );

            if ( ! $url ) {
                $results[] = array( 'url' => $raw_url, 'status' => 'skipped', 'reason' => __( 'Invalid URL after sanitization', 'schemafy' ) );
                $skipped++;
                continue;
            }

            if (!$this->is_same_site_meta_url($url)) {
                $results[] = array( 'url' => $url, 'status' => 'skipped', 'reason' => __( 'URL is outside this WordPress site', 'schemafy' ) );
                $skipped++;
                continue;
            }

            // Skip rows where both fields are empty (nothing to store)
            if ( '' === $title && '' === $desc ) {
                $results[] = array( 'url' => $url, 'status' => 'skipped', 'reason' => __( 'Both title and description are empty', 'schemafy' ) );
                $skipped++;
                continue;
            }

            $length_error = $this->validate_meta_lengths($title, $desc);
            if ($length_error) {
                $results[] = array( 'url' => $url, 'status' => 'skipped', 'reason' => $length_error );
                $skipped++;
                continue;
            }

            $existing_key = $this->find_custom_meta_key($meta_tags, $url);
            $is_update = $existing_key !== '';
            $existing  = $is_update ? $meta_tags[ $existing_key ] : array();

            if ($existing_key && $existing_key !== $url) {
                unset($meta_tags[$existing_key]);
            }

            // Resolve post ID for post-meta storage (best-effort; 0 = no match)
            $post_id = $this->resolve_meta_post_id($url, 0);

            if ( $post_id > 0 ) {
                if ( ! isset( $post_meta_updates[$post_id] ) ) {
                    $post_meta_updates[$post_id] = array(
                        'title'       => null,
                        'description' => null,
                    );
                }

                if ( '' !== $title ) {
                    $post_meta_updates[$post_id]['title'] = $title;
                }

                if ( '' !== $desc ) {
                    $post_meta_updates[$post_id]['description'] = $desc;
                }
            }

            // Merge into options table: keep existing value for fields not provided
            $meta_tags[ $url ] = array(
                'title'       => '' !== $title ? $title : ( $existing['title']       ?? '' ),
                'description' => '' !== $desc  ? $desc  : ( $existing['description'] ?? '' ),
                'post_id'     => $post_id > 0  ? $post_id : ( $existing['post_id']   ?? 0 ),
                'updated_at'  => current_time( 'mysql' ),
            );
            $expected_meta_tags[$url] = $meta_tags[$url];

            $results[] = array(
                'url'     => $url,
                'status'  => 'saved',
                'action'  => $is_update ? 'updated' : 'created',
                'post_id' => $post_id,
            );

            if ( $is_update ) $updated++;
            else              $saved++;
        }

        // Persist to DB
        $save_ok = update_option( self::META_OPTION_NAME, $meta_tags, false );

        // update_option() returns false both on "no change" and on DB failure.
        // Re-read to confirm the write landed when we actually processed rows.
        if ( ! $save_ok && ( $saved + $updated ) > 0 ) {
            $verify  = get_option( self::META_OPTION_NAME, array() );
            $save_ok = is_array( $verify );

            if ( $save_ok ) {
                foreach ( $expected_meta_tags as $expected_url => $expected ) {
                    if (
                        ! isset( $verify[$expected_url] )
                        || (string) ( $verify[$expected_url]['title'] ?? '' ) !== (string) ( $expected['title'] ?? '' )
                        || (string) ( $verify[$expected_url]['description'] ?? '' ) !== (string) ( $expected['description'] ?? '' )
                    ) {
                        $save_ok = false;
                        break;
                    }
                }
            }
        }

        if ( ! $save_ok && ( $saved + $updated ) > 0 ) {
            $this->release_meta_lock($lock_token);

            wp_send_json_error( array(
                'message'   => sprintf(
                    /* translators: %d: processed rows count. */
                    __( 'Processed %d row(s) but could not persist to the database. The stored option may be too large - try importing in smaller batches.', 'schemafy' ),
                    $saved + $updated
                ),
                'processed' => $saved + $updated,
                'skipped'   => $skipped,
            ) );
        }

        if ( $save_ok ) {
            foreach ( $post_meta_updates as $update_post_id => $values ) {
                list( $existing_title, $existing_desc ) = SCHEMAFY_Meta_Store::get( $update_post_id );

                SCHEMAFY_Meta_Store::set(
                    $update_post_id,
                    ( isset( $values['title'] ) && null !== $values['title'] ) ? $values['title'] : $existing_title,
                    ( isset( $values['description'] ) && null !== $values['description'] ) ? $values['description'] : $existing_desc
                );
            }
        }

        $this->release_meta_lock($lock_token);

        wp_send_json_success( array(
            'saved'   => $saved + $updated,
            'new'     => $saved,
            'updated' => $updated,
            'skipped' => $skipped,
            'total'   => count( $rows ),
            'results' => $results,
        ) );
    }

    /**
     * AJAX Handler: Delete/reset meta tags
     */
    public function schemafy_delete_meta_tags() {
        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Unauthorized', 'schemafy')), 401);
        }

        // Verify nonce
        $nonce = sanitize_text_field($this->get_meta_request_string('nonce'));
        if (!$nonce || !wp_verify_nonce($nonce, 'schemafy_nonce')) {
            wp_send_json_error(array('message' => __('Invalid nonce', 'schemafy')), 403);
        }

        $url = $this->normalize_meta_url($this->get_meta_request_string('url'));
        $post_id = $this->get_meta_request_absint('post_id');

        if (!$url) {
            wp_send_json_error(array('message' => __('A valid URL is required.', 'schemafy')), 400);
        }

        if (!$this->is_same_site_meta_url($url)) {
            wp_send_json_error(array('message' => __('Only URLs from this WordPress site can be managed here.', 'schemafy')), 400);
        }

        $post_id = $this->resolve_meta_post_id($url, $post_id);
        $lock_token = $this->acquire_meta_lock();
        if (!$lock_token) {
            wp_send_json_error(array('message' => __('Schemafy is already saving meta tags. Please retry in a moment.', 'schemafy')), 423);
        }

        $meta_tags = get_option(self::META_OPTION_NAME, array());
        if (!is_array($meta_tags)) {
            $meta_tags = array();
        }

        $removed_option_meta = false;
        foreach ($this->get_meta_url_candidates($url) as $candidate) {
            if (isset($meta_tags[$candidate])) {
                unset($meta_tags[$candidate]);
                $removed_option_meta = true;
            }
        }

        $option_cleared = true;
        if ($removed_option_meta) {
            $option_cleared = update_option(self::META_OPTION_NAME, $meta_tags, false);

            if (!$option_cleared) {
                $verify = get_option(self::META_OPTION_NAME, array());
                $option_cleared = is_array($verify) && '' === $this->find_custom_meta_key($verify, $url);
            }
        }

        if (!$option_cleared) {
            $this->release_meta_lock($lock_token);
            wp_send_json_error(array('message' => __('Error clearing meta tags', 'schemafy')), 500);
        }

        if ($post_id > 0) {
            SCHEMAFY_Meta_Store::delete($post_id);
        }

        $this->release_meta_lock($lock_token);

        wp_send_json_success(array(
            'message' => __('Meta tags reset to default', 'schemafy'),
            'url' => $url,
            'post_id' => $post_id,
        ));
    }

    /**
     * Get custom meta tags from database
     */
    private function get_custom_meta_from_db($url) {
        $meta_tags = get_option(self::META_OPTION_NAME, array());

        if (!is_array($meta_tags)) {
            return array('title' => '', 'description' => '');
        }

        $key = $this->find_custom_meta_key($meta_tags, $url);
        if ($key !== '') {
            return array(
                'title' => isset($meta_tags[$key]['title']) ? $meta_tags[$key]['title'] : '',
                'description' => isset($meta_tags[$key]['description']) ? $meta_tags[$key]['description'] : '',
            );
        }

        return array('title' => '', 'description' => '');
    }

    /**
     * The value to put in the panel's editable title/description fields.
     *
     * Post meta is the source of truth whenever the URL resolves to a post.
     * That is where SCHEMAFY_Meta_Store writes, and when an SEO plugin owns
     * these fields it is the very row that plugin reads — so a title typed in
     * Rank Math appears here, and one typed here appears in Rank Math. Reading
     * the URL-keyed option first is what made that one-directional: Rank Math
     * cannot write an option, so its edits never reached this screen.
     *
     * The option is still consulted, as a fallback, for the two things only it
     * can hold: URLs that are not posts (archives, hand-entered URLs), and
     * values saved before an SEO plugin was installed.
     *
     * @param string $url     Normalised URL.
     * @param int    $post_id Resolved post ID, 0 when the URL is not a post.
     * @return array{title:string,description:string}
     */
    private function get_effective_custom_meta($url, $post_id) {
        $option = $this->get_custom_meta_from_db($url);

        if ($post_id <= 0) {
            return $option;
        }

        list($title, $description) = SCHEMAFY_Meta_Store::get($post_id);

        // When an SEO plugin owns these fields its row is the whole truth,
        // empty included. Falling back here would resurrect an old value the
        // moment the user cleared the field in that plugin's own panel.
        if (SCHEMAFY_Meta_Store::owner()) {
            return array('title' => $title, 'description' => $description);
        }

        // Schemafy owns the fields: the option and Schemafy's post meta are both
        // its own storage, so the fallback only rescues values from before this
        // screen started keeping the two in step.
        return array(
            'title'       => $title !== '' ? $title : $option['title'],
            'description' => $description !== '' ? $description : $option['description'],
        );
    }

    /**
     * Determine the source of meta tags
     */
    private function determine_meta_source($post_id, $type, $url = '') {
        $index = ($type === 'title') ? 0 : 1;

        if ($post_id > 0) {
            $stored = SCHEMAFY_Meta_Store::get($post_id);

            if ($stored[$index] !== '') {
                // Name where the value actually lives: the active SEO plugin's
                // rows when one owns these fields, Schemafy's own otherwise.
                // Detection comes from the store rather than being re-tested
                // here, so this label cannot disagree with what the rest of the
                // plugin reads and writes — it used to, by only looking for a
                // RankMath class and never RANK_MATH_VERSION.
                $owner = SCHEMAFY_Meta_Store::owner_name();

                return $owner !== '' ? $owner : 'Schemafy';
            }
        }

        if ($url !== '') {
            $custom_meta = $this->get_custom_meta_from_db($url);
            $custom_key = ($type === 'title') ? 'title' : 'description';
            if (!empty($custom_meta[$custom_key])) {
                return 'Schemafy';
            }
        }

        // A plugin that renders these tags but keeps them where Schemafy cannot
        // read them (All in One SEO's own table, Slim SEO's packed array) is
        // still the source, so name it rather than blaming WordPress.
        foreach (SCHEMAFY_Meta_Store::active_plugins() as $seo) {
            if (empty($seo['owns_storage'])) {
                return $seo['name'];
            }
        }

        return 'WordPress Default';
    }

    /**
     * Read a scalar request value safely for Meta Tags AJAX handlers.
     *
     * Nonce is verified by every calling handler before this runs, and each
     * caller sanitizes the returned value with the field-appropriate function
     * (sanitize_text_field / sanitize_textarea_field / normalize_meta_url).
     *
     * @param string $key Request key.
     * @return string
     */
    private function get_meta_request_string($key) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by calling handler
        if (!isset($_POST[$key]) || !is_scalar($_POST[$key])) {
            return '';
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified by caller; value sanitized at each call site
        return (string) wp_unslash($_POST[$key]);
    }

    /**
     * Read a positive integer request value safely for Meta Tags AJAX handlers.
     *
     * @param string $key Request key.
     * @return int
     */
    private function get_meta_request_absint($key) {
        $value = $this->get_meta_request_string($key);

        return $value === '' ? 0 : absint($value);
    }

    /**
     * Normalize a meta-manager URL so slash variants do not create duplicate records.
     *
     * @param string $url URL to normalize.
     * @return string
     */
    private function normalize_meta_url($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }

        $url = esc_url_raw($url);
        if (!$url) {
            return '';
        }

        $parts = wp_parse_url($url);
        if (empty($parts['scheme']) || empty($parts['host']) || !in_array(strtolower($parts['scheme']), array('http', 'https'), true)) {
            return '';
        }

        $scheme = strtolower($parts['scheme']);
        $host = strtolower($parts['host']);
        $path = isset($parts['path']) ? preg_replace('#/+#', '/', $parts['path']) : '/';
        $path = '/' . ltrim((string) $path, '/');
        $path = $path === '/' ? '/' : untrailingslashit($path);
        $port = '';

        if (!empty($parts['port'])) {
            $default_port = $scheme === 'https' ? 443 : 80;
            if ((int) $parts['port'] !== $default_port) {
                $port = ':' . absint($parts['port']);
            }
        }

        $query = isset($parts['query']) && $parts['query'] !== '' ? '?' . $parts['query'] : '';

        return esc_url_raw($scheme . '://' . $host . $port . $path . $query);
    }

    /**
     * Whether a URL belongs to this WordPress site.
     *
     * @param string $url URL to check.
     * @return bool
     */
    private function is_same_site_meta_url($url) {
        $url_parts = wp_parse_url($url);
        $home_parts = wp_parse_url(home_url('/'));

        if (empty($url_parts['host']) || empty($home_parts['host'])) {
            return false;
        }

        return $this->normalize_meta_host($url_parts['host']) === $this->normalize_meta_host($home_parts['host']);
    }

    /**
     * Normalize hosts for www/non-www comparisons.
     *
     * @param string $host Host.
     * @return string
     */
    private function normalize_meta_host($host) {
        return preg_replace('/^www\./i', '', strtolower((string) $host));
    }

    /**
     * Return candidate keys used by older and newer URL storage.
     *
     * @param string $url URL.
     * @return array
     */
    private function get_meta_url_candidates($url) {
        $normalized = $this->normalize_meta_url($url);
        $candidates = array_filter(array(esc_url_raw($url), $normalized));

        foreach ($candidates as $candidate) {
            if (strpos($candidate, '?') !== false) {
                continue;
            }

            $candidates[] = trailingslashit($candidate);
            $candidates[] = untrailingslashit($candidate);
        }

        if (empty($candidates) && $url !== '') {
            $candidates[] = (string) $url;
        }

        return array_values(array_unique(array_filter($candidates)));
    }

    /**
     * Find an existing stored URL key across normalized URL variants.
     *
     * @param array  $meta_tags Stored meta tags.
     * @param string $url URL.
     * @return string
     */
    private function find_custom_meta_key($meta_tags, $url) {
        if (!is_array($meta_tags)) {
            return '';
        }

        foreach ($this->get_meta_url_candidates($url) as $candidate) {
            if (isset($meta_tags[$candidate])) {
                return $candidate;
            }
        }

        return '';
    }

    /**
     * Resolve a submitted post ID only when it belongs to the submitted URL.
     *
     * @param string $url URL.
     * @param int    $submitted_post_id Submitted post ID.
     * @return int
     */
    private function resolve_meta_post_id($url, $submitted_post_id) {
        $submitted_post_id = absint($submitted_post_id);
        $url_candidates = $this->get_meta_url_candidates($url);

        if ($submitted_post_id > 0) {
            $post = get_post($submitted_post_id);
            $permalink = $post ? get_permalink($submitted_post_id) : '';

            if ($permalink && in_array($this->normalize_meta_url($permalink), $url_candidates, true)) {
                return $submitted_post_id;
            }

            $front_page_id = (int) get_option('page_on_front');
            if ($front_page_id > 0 && $submitted_post_id === $front_page_id && in_array($this->normalize_meta_url(home_url('/')), $url_candidates, true)) {
                return $submitted_post_id;
            }
        }

        $resolved = url_to_postid($url);
        if ($resolved > 0) {
            return (int) $resolved;
        }

        $front_page_id = (int) get_option('page_on_front');
        if ($front_page_id > 0 && in_array($this->normalize_meta_url(home_url('/')), $url_candidates, true)) {
            return $front_page_id;
        }

        return 0;
    }

    /**
     * Validate meta field lengths server-side.
     *
     * @param string $title Meta title.
     * @param string $description Meta description.
     * @return string
     */
    private function validate_meta_lengths($title, $description) {
        if ($this->meta_string_length($title) > self::META_MAX_TITLE_LENGTH) {
            // translators: %d is a value inserted at runtime.
            return sprintf(__('Meta title is too long. Maximum length is %d characters.', 'schemafy'), self::META_MAX_TITLE_LENGTH);
        }

        if ($this->meta_string_length($description) > self::META_MAX_DESCRIPTION_LENGTH) {
            // translators: %d is a value inserted at runtime.
            return sprintf(__('Meta description is too long. Maximum length is %d characters.', 'schemafy'), self::META_MAX_DESCRIPTION_LENGTH);
        }

        return '';
    }

    /**
     * Count characters with multibyte support when available.
     *
     * @param string $value Text.
     * @return int
     */
    private function meta_string_length($value) {
        return function_exists('mb_strlen') ? mb_strlen((string) $value, 'UTF-8') : strlen((string) $value);
    }

    /**
     * Acquire a short write lock for meta-tag saves.
     *
     * @param int $ttl Lock lifetime in seconds.
     * @return string|false
     */
    private function acquire_meta_lock($ttl = 20) {
        $token = wp_generate_uuid4();
        $lock = get_option(self::META_LOCK_OPTION);

        if (is_array($lock) && !empty($lock['expires']) && time() > absint($lock['expires'])) {
            delete_option(self::META_LOCK_OPTION);
        }

        $created = add_option(self::META_LOCK_OPTION, array(
            'token' => $token,
            'expires' => time() + absint($ttl),
        ), '', 'no');

        return $created ? $token : false;
    }

    /**
     * Release a meta-tag write lock owned by this request.
     *
     * @param string $token Lock token.
     */
    private function release_meta_lock($token) {
        $lock = get_option(self::META_LOCK_OPTION);

        if (is_array($lock) && isset($lock['token']) && hash_equals((string) $lock['token'], (string) $token)) {
            delete_option(self::META_LOCK_OPTION);
        }
    }

    /**
     * AJAX Handler: Get bulk health stats for quick actions bar
     */
    public function schemafy_get_bulk_health_stats() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Unauthorized', 401);
        }

        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'schemafy_nonce')) {
            wp_send_json_error('Invalid nonce', 403);
        }

        $all_schemas = $this->database->get_all_schemas();

        // Get total pages (count only, no objects)
        $post_count = wp_count_posts('post');
        $page_count = wp_count_posts('page');
        $total_pages = (isset($post_count->publish) ? intval($post_count->publish) : 0) +
                       (isset($page_count->publish) ? intval($page_count->publish) : 0);

        // Get URLs with schemas (using array notation since get_all_schemas returns ARRAY_A)
        $schema_urls = array();
        foreach ($all_schemas as $schema) {
            $schema_urls[] = trailingslashit($schema['url']);
        }
        $unique_urls = count(array_unique($schema_urls));

        // Calculate coverage (cap at 100%)
        $coverage = $total_pages > 0 ? min(100, round(($unique_urls / $total_pages) * 100)) : 0;

        // Count valid/error schemas (json_decode doesn't throw exceptions)
        $valid = 0;
        $errors = 0;
        foreach ($all_schemas as $schema) {
            $data = json_decode($schema['schema_data'], true);
            if (is_array($data) && isset($data['@context']) && isset($data['@type'])) {
                $valid++;
            } else {
                $errors++;
            }
        }

        // Count orphans - use IDs only for memory efficiency
        $orphans = 0;
        global $wpdb;

        // Get all published post/page URLs using direct query for efficiency
        $post_ids = $wpdb->get_col(
            "SELECT ID FROM {$wpdb->posts}
             WHERE post_type IN ('post', 'page')
             AND post_status = 'publish'
             LIMIT 5000"
        );

        $valid_urls = array();
        foreach ($post_ids as $post_id) {
            $valid_urls[] = trailingslashit(get_permalink($post_id));
        }
        $valid_urls[] = trailingslashit(home_url('/'));

        foreach ($all_schemas as $schema) {
            if (!in_array(trailingslashit($schema['url']), $valid_urls)) {
                $orphans++;
            }
        }

        wp_send_json_success(array(
            'coverage' => $coverage,
            'valid' => $valid,
            'errors' => $errors,
            'orphans' => $orphans
        ));
    }

    // =========================================================================
    // AutoGenerator AJAX Handlers
    // =========================================================================

    /**
     * AJAX Handler: Save plugin compatibility settings
     */
    public function schemafy_save_compatibility() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error(__('Unauthorized', 'schemafy'));
        }
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'schemafy_nonce')) {
            wp_send_json_error(__('Invalid nonce', 'schemafy'));
        }

        $settings = ( isset($_POST['settings']) && is_array($_POST['settings']) ) ? wp_unslash($_POST['settings']) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each value cast to 0/1 by !empty() below
        $clean = array();
        $valid_keys = array(
            'disable_yoast_schema',
            'disable_rankmath_schema',
            'disable_aioseo_schema',
            'disable_seopress_schema',
            'disable_tsf_schema',
            'disable_woocommerce_schema',
            'strip_embedded_schemas',
            'disable_other_sitemaps',
        );

        foreach ($valid_keys as $key) {
            $clean[$key] = !empty($settings[$key]) ? 1 : 0;
        }

        update_option('schemafy_compatibility', $clean);
        wp_send_json_success(array('message' => __('Compatibility settings saved', 'schemafy')));
    }
}
