<?php
// phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- date() used for internal non-display formatting; timezone managed at application level; direct filesystem calls required for Instinctor standalone storage driver; input sanitization handled by Instinctor sanitization layer on save// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Nonce verification is handled by check_ajax_referer() which calls check_ajax_referer() in WP context and a HMAC token verifier in standalone context. Direct wp_verify_nonce() calls are absent by design.

declare(strict_types=1);

namespace Instinctor\Admin;

use Instinctor\Core\Config;
use Instinctor\Core\DB;
use Instinctor\Core\Storage;
use Instinctor\Core\Auth;
use Instinctor\Core\Session;
use Instinctor\Core\Mailer;
use Instinctor\Services\PageService;

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

/**
 * Admin Controller - orchestrates the WordPress admin interface
 */
class AdminController
{
    public function __construct()
    {
        add_action('admin_menu', [$this, 'registerMenus']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
        add_action('admin_init', [$this, 'handleTemplateFormActions']);
        // Upgrade notice removed — all features unlocked
        
        // Auto-install roles/caps on upgrade (no reactivation needed)
        add_action('admin_init', [$this, 'maybeInstallRoles'], 1);
        
        // Impulse WordPress Pages integration
        add_action('admin_init', [$this, 'redirectImpulseEdit'], 1);
        add_filter('page_row_actions', [$this, 'addImpulseRowAction'], 10, 2);
        add_filter('get_edit_post_link', [$this, 'filterImpulseEditLink'], 10, 3);
        add_filter('use_block_editor_for_post', [$this, 'disableGutenbergForImpulse'], 10, 2);
        add_action('add_meta_boxes', [$this, 'addImpulseMetaBox']);
        
        // AJAX handlers for variations
        $this->registerAjax('instinctor_generate_variations', [$this, 'ajaxGenerateVariations']);
        $this->registerAjax('instinctor_save_variations', [$this, 'ajaxSaveVariations']);
        $this->registerAjax('instinctor_delete_variation', [$this, 'ajaxDeleteVariation']);
        
        // AJAX handlers for order management
        $this->registerAjax('instinctor_add_order_note', [$this, 'ajaxAddOrderNote']);
        $this->registerAjax('instinctor_delete_order_note', [$this, 'ajaxDeleteOrderNote']);
        $this->registerAjax('instinctor_delete_order', [$this, 'ajaxDeleteOrder']);
        $this->registerAjax('instinctor_delete_client', [$this, 'ajaxDeleteClient']);
        $this->registerAjax('instinctor_save_tracking', [$this, 'ajaxSaveTracking']);
        $this->registerAjax('instinctor_send_tracking_email', [$this, 'ajaxSendTrackingEmail']);
        $this->registerAjax('instinctor_process_refund', [$this, 'ajaxProcessRefund']);
        $this->registerAjax('instinctor_update_order_status', [$this, 'ajaxUpdateOrderStatus']);
        
        // Dashboard workspace AJAX
        $this->registerAjax('instinctor_get_order_detail',    [$this, 'ajaxGetOrderDetail']);
        $this->registerAjax('instinctor_get_customer_detail', [$this, 'ajaxGetCustomerDetail']);
        $this->registerAjax('instinctor_update_member',       [$this, 'ajaxUpdateMember']);
        $this->registerAjax('instinctor_get_customers',       [$this, 'ajaxGetCustomers']);
        $this->registerAjax('instinctor_get_client_note',     [$this, 'ajaxGetClientNote']);
        $this->registerAjax('instinctor_save_client_note',    [$this, 'ajaxSaveClientNote']);

        // Salon CRM (client profile: before/after photos + color formulas + tags)
        $this->registerAjax('instinctor_client_photo_add',     [$this, 'ajaxClientPhotoAdd']);
        $this->registerAjax('instinctor_client_photo_delete',  [$this, 'ajaxClientPhotoDelete']);
        $this->registerAjax('instinctor_client_formula_save',  [$this, 'ajaxClientFormulaSave']);
        $this->registerAjax('instinctor_client_formula_delete',[$this, 'ajaxClientFormulaDelete']);
        $this->registerAjax('instinctor_client_meta_save',     [$this, 'ajaxClientMetaSave']);

        // Blog — simple post editor (title + cover + body → a flow-mode post)
        $this->registerAjax('instinctor_save_post',           [$this, 'ajaxSavePost']);
        $this->registerAjax('instinctor_get_post',            [$this, 'ajaxGetPost']);

        // Marketing workspace AJAX
        $this->registerAjax('instinctor_get_coupon_detail', [$this, 'ajaxGetCouponDetail']);
        $this->registerAjax('instinctor_save_coupon', [$this, 'ajaxSaveCoupon']);
        $this->registerAjax('instinctor_delete_coupon', [$this, 'ajaxDeleteCoupon']);
        $this->registerAjax('instinctor_get_all_coupons', [$this, 'ajaxGetAllCoupons']);
        $this->registerAjax('instinctor_get_gift_cards', [$this, 'ajaxGetGiftCards']);
        $this->registerAjax('instinctor_get_gift_card_detail', [$this, 'ajaxGetGiftCardDetail']);
        $this->registerAjax('instinctor_create_gift_card', [$this, 'ajaxCreateGiftCard']);
        $this->registerAjax('instinctor_send_gift_card', [$this, 'ajaxSendGiftCard']);
        $this->registerAjax('instinctor_deactivate_gift_card', [$this, 'ajaxDeactivateGiftCard']);
        $this->registerAjax('instinctor_get_loyalty_settings', [$this, 'ajaxGetLoyaltySettings']);
        $this->registerAjax('instinctor_save_loyalty_settings', [$this, 'ajaxSaveLoyaltySettings']);
        $this->registerAjax('instinctor_get_loyalty_stats', [$this, 'ajaxGetLoyaltyStats']);
        $this->registerAjax('instinctor_adjust_loyalty_points', [$this, 'ajaxAdjustLoyaltyPoints']);
        
        // AJAX handlers for currency management
        $this->registerAjax('instinctor_save_currency', [$this, 'ajaxSaveCurrency']);
        $this->registerAjax('instinctor_delete_currency', [$this, 'ajaxDeleteCurrency']);
        $this->registerAjax('instinctor_update_exchange_rates', [$this, 'ajaxUpdateExchangeRates']);
        $this->registerAjax('instinctor_save_option', [$this, 'ajaxSaveOption']);
        
        // Email template toggle
        $this->registerAjax('instinctor_toggle_email_template', [$this, 'ajaxToggleEmailTemplate']);
        $this->registerAjax('instinctor_save_footer_settings', [$this, 'ajaxSaveFooterSettings']);
        
        // Intercept WordPress password reset email when our template is enabled
        add_filter('retrieve_password_message', [$this, 'handlePasswordResetEmail'], 10, 4);
        
        // Add-ons toggle
        $this->registerAjax('instinctor_toggle_addon', [$this, 'ajaxToggleAddon']);
        
        // AfterShip settings
        $this->registerAjax('instinctor_save_aftership', [$this, 'ajaxSaveAfterShip']);
        $this->registerAjax('instinctor_save_aftership_settings', [$this, 'ajaxSaveAfterShipSettings']);
        $this->registerAjax('instinctor_get_tracking_settings', [$this, 'ajaxGetTrackingSettings']);
        $this->registerAjax('instinctor_test_aftership', [$this, 'ajaxTestAfterShip']);
        
        // Newsletter subscription (public)
        $this->registerAjax('instinctor_newsletter_subscribe', [$this, 'ajaxNewsletterSubscribe']);
        $this->registerAjax('nopriv_instinctor_newsletter_subscribe', [$this, 'ajaxNewsletterSubscribe']);
        
        // v1.3.0 AJAX handlers
        $this->registerAjax('instinctor_add_tracking', [$this, 'ajaxAddTracking']);
        $this->registerAjax('instinctor_update_tracking_status', [$this, 'ajaxUpdateTrackingStatus']);
        $this->registerAjax('instinctor_delete_tracking', [$this, 'ajaxDeleteTracking']);
        $this->registerAjax('instinctor_update_stock', [$this, 'ajaxUpdateStock']);
        $this->registerAjax('instinctor_export_inventory', [$this, 'ajaxExportInventory']);
        $this->registerAjax('instinctor_update_rma_status', [$this, 'ajaxUpdateRMAStatus']);
        $this->registerAjax('instinctor_get_rmas', [$this, 'ajaxGetRMAs']);
        $this->registerAjax('instinctor_get_rma', [$this, 'ajaxGetRMA']);
        $this->registerAjax('instinctor_approve_rma', [$this, 'ajaxApproveRMA']);
        $this->registerAjax('instinctor_reject_rma', [$this, 'ajaxRejectRMA']);
        $this->registerAjax('instinctor_rma_refund_now', [$this, 'ajaxRMARefundNow']);
        $this->registerAjax('instinctor_get_rma_settings', [$this, 'ajaxGetRMASettings']);
        $this->registerAjax('instinctor_save_rma_settings', [$this, 'ajaxSaveRMASettings']);

        $this->registerAjax('instinctor_print_invoice', [$this, 'ajaxPrintInvoice']);
        $this->registerAjax('instinctor_print_packing_slip', [$this, 'ajaxPrintPackingSlip']);
        
        // Pricing Table - Products and Subscriptions
        $this->registerAjax('instinctor_get_products', [$this, 'ajaxGetProducts']);
        $this->registerAjax('instinctor_get_subscription_plans', [$this, 'ajaxGetSubscriptionPlans']);


        // AI Recommendations AJAX        
        // v1.7.0 - UX improvements
        $this->registerAjax('instinctor_bulk_products', [$this, 'ajaxBulkProducts']);
        $this->registerAjax('instinctor_inline_edit_product', [$this, 'ajaxInlineEditProduct']);
        $this->registerAjax('instinctor_update_product_status', [$this, 'ajaxUpdateProductStatus']);
        $this->registerAjax('instinctor_global_search', [$this, 'ajaxGlobalSearch']);
        $this->registerAjax('instinctor_bulk_orders', [$this, 'ajaxBulkOrders']);
        $this->registerAjax('instinctor_get_orders_list', [$this, 'ajaxGetOrdersList']);
        $this->registerAjax('instinctor_reorder_categories', [$this, 'ajaxReorderCategories']);
        $this->registerAjax('instinctor_inline_update', [$this, 'ajaxInlineUpdate']);
        $this->registerAjax('instinctor_empty_trash', [$this, 'ajaxEmptyTrash']);
        
        // Impulse Products Workspace
        $this->registerAjax('instinctor_get_products', [$this, 'ajaxImpulseGetProducts']);
        $this->registerAjax('instinctor_get_categories', [$this, 'ajaxImpulseGetCategories']);
        $this->registerAjax('instinctor_create_category', [$this, 'ajaxImpulseCreateCategory']);
        $this->registerAjax('instinctor_update_category', [$this, 'ajaxImpulseUpdateCategory']);
        $this->registerAjax('instinctor_delete_category', [$this, 'ajaxImpulseDeleteCategory']);
        $this->registerAjax('instinctor_reorder_categories', [$this, 'ajaxImpulseReorderCategories']);
        $this->registerAjax('instinctor_move_category', [$this, 'ajaxImpulseMoveCategory']);
        $this->registerAjax('instinctor_get_inventory_stats', [$this, 'ajaxImpulseGetInventoryStats']);
        $this->registerAjax('instinctor_save_product', [$this, 'ajaxImpulseSaveProduct']);
        $this->registerAjax('instinctor_delete_product', [$this, 'ajaxImpulseDeleteProduct']);
        $this->registerAjax('instinctor_bulk_products', [$this, 'ajaxImpulseBulkProducts']);
        $this->registerAjax('instinctor_reorder_products', [$this, 'ajaxImpulseReorderProducts']);
        $this->registerAjax('instinctor_update_product_status', [$this, 'ajaxImpulseUpdateProductStatus']);
        $this->registerAjax('instinctor_update_product_featured', [$this, 'ajaxImpulseUpdateProductFeatured']);
        $this->registerAjax('instinctor_trash_product', [$this, 'ajaxImpulseTrashProduct']);
        $this->registerAjax('instinctor_restore_product', [$this, 'ajaxImpulseRestoreProduct']);
        $this->registerAjax('instinctor_duplicate_product', [$this, 'ajaxImpulseDuplicateProduct']);
        $this->registerAjax('instinctor_save_settings', [$this, 'ajaxImpulseSaveSettings']);
        $this->registerAjax('instinctor_save_store_options', [$this, 'ajaxImpulseSaveStoreOptions']);
        
        // `instinctor_apply_update` was retired in v2.1.1354. The standalone
        // self-update path lives at /admin/updates (Services\AutoUpdater);
        // WP Pro updates go through WP's native transient + Plugin_Upgrader.

        $this->registerAjax('instinctor_set_frontpage', [$this, 'ajaxImpulseSetFrontpage']);
        
        // Email template handlers
        $this->registerAjax('instinctor_toggle_email_template', [$this, 'ajaxImpulseToggleEmailTemplate']);
        $this->registerAjax('instinctor_get_email_template', [$this, 'ajaxImpulseGetEmailTemplate']);
        $this->registerAjax('instinctor_save_email_template', [$this, 'ajaxImpulseSaveEmailTemplate']);
        $this->registerAjax('instinctor_send_test_email', [$this, 'ajaxImpulseSendTestEmail']);
        $this->registerAjax('instinctor_get_email_footer_settings', [$this, 'ajaxImpulseGetEmailFooterSettings']);
        $this->registerAjax('instinctor_save_email_footer_settings', [$this, 'ajaxImpulseSaveEmailFooterSettings']);
        $this->registerAjax('instinctor_activate_addon', [$this, 'ajaxImpulseActivateAddon']);
        $this->registerAjax('instinctor_deactivate_addon', [$this, 'ajaxImpulseDeactivateAddon']);
        $this->registerAjax('instinctor_activate_license', [$this, 'ajaxImpulseActivateLicense']);
        $this->registerAjax('instinctor_proxy_checkout', [$this, 'ajaxImpulseProxyCheckout']);
        $this->registerAjax('instinctor_bulk_categories', [$this, 'ajaxBulkCategories']);
        
        // Card Designer AJAX
        $this->registerAjax('instinctor_save_card_design', [$this, 'ajaxSaveCardDesign']);
        $this->registerAjax('instinctor_save_card_template', [$this, 'ajaxSaveCardTemplate']);
        $this->registerAjax('instinctor_delete_card_template', [$this, 'ajaxDeleteCardTemplate']);
        $this->registerAjax('instinctor_load_card_template', [$this, 'ajaxLoadCardTemplate']);
        $this->registerAjax('instinctor_get_card_templates', [$this, 'ajaxGetCardTemplates']);
        
        // Header/Footer Global AJAX
        $this->registerAjax('instinctor_save_global_header', [$this, 'ajaxSaveGlobalHeader']);
        $this->registerAjax('instinctor_get_global_header', [$this, 'ajaxGetGlobalHeader']);
        $this->registerAjax('instinctor_save_global_footer', [$this, 'ajaxSaveGlobalFooter']);
        $this->registerAjax('instinctor_get_global_footer', [$this, 'ajaxGetGlobalFooter']);
        
        // Multi-Language AJAX
        
        // Add-ons AJAX (legacy per-addon actions removed — UI uses instinctor_activate_addon / instinctor_deactivate_addon)
        
        // Subscription AJAX
        $this->registerAjax('instinctor_create_subscription', [$this, 'ajaxCreateSubscription']);
        $this->registerAjax('instinctor_update_subscription_status', [$this, 'ajaxUpdateSubscriptionStatus']);
        $this->registerAjax('instinctor_save_subscription_settings', [$this, 'ajaxSaveSubscriptionSettings']);
        
        // Booking AJAX
        $this->registerAjax('instinctor_get_bookings_calendar', [$this, 'ajaxGetBookingsCalendar']);
        $this->registerAjax('instinctor_get_bookings_list', [$this, 'ajaxGetBookingsList']);
        $this->registerAjax('instinctor_get_form_submissions', [$this, 'ajaxGetFormSubmissions']);
        $this->registerAjax('instinctor_get_bookings_stats', [$this, 'ajaxGetBookingsStats']);
        $this->registerAjax('instinctor_get_services', [$this, 'ajaxGetServices']);
        $this->registerAjax('instinctor_save_service', [$this, 'ajaxSaveService']);
        $this->registerAjax('instinctor_delete_service', [$this, 'ajaxDeleteService']);
        // Staff / Team management (Phase 1)
        $this->registerAjax('instinctor_get_staff', [$this, 'ajaxGetStaff']);
        $this->registerAjax('instinctor_save_staff', [$this, 'ajaxSaveStaff']);
        $this->registerAjax('instinctor_delete_staff', [$this, 'ajaxDeleteStaff']);
        $this->registerAjax('instinctor_get_booking', [$this, 'ajaxGetBooking']);
        $this->registerAjax('instinctor_create_booking', [$this, 'ajaxCreateBooking']);
        $this->registerAjax('instinctor_update_booking_status', [$this, 'ajaxUpdateBookingStatus']);
        $this->registerAjax('instinctor_update_booking_staff', [$this, 'ajaxUpdateBookingStaff']);
        $this->registerAjax('instinctor_save_booking_settings', [$this, 'ajaxSaveBookingSettings']);
        $this->registerAjax('instinctor_complete_onboarding', [$this, 'ajaxCompleteOnboarding']);
        // Instructor vertical — class schedule CRUD
        

        // Report Problem AJAX
        
        // Workspace preferences (per-user)
        $this->registerAjax('instinctor_save_workspace_prefs', [$this, 'ajaxSaveWorkspacePrefs']);
        $this->registerAjax('instinctor_dismiss_upsell', [$this, 'ajaxDismissUpsell']);
        
        // Team/Role management
        $this->registerAjax('instinctor_get_team', [$this, 'ajaxGetTeam']);
        $this->registerAjax('instinctor_set_user_role', [$this, 'ajaxSetUserRole']);

        // Digital Downloads
        $this->registerAjax('instinctor_get_product_downloads', [$this, 'ajaxGetProductDownloads']);
        $this->registerAjax('instinctor_save_product_download', [$this, 'ajaxSaveProductDownload']);
        $this->registerAjax('instinctor_delete_product_download', [$this, 'ajaxDeleteProductDownload']);
        $this->registerAjax('instinctor_get_customer_downloads', [$this, 'ajaxGetCustomerDownloads']);
        $this->registerAjax('instinctor_get_download_stats', [$this, 'ajaxGetDownloadStats']);
        $this->registerAjax('instinctor_toggle_product_downloadable', [$this, 'ajaxToggleProductDownloadable']);
        $this->registerAjax('instinctor_save_dd_settings', [$this, 'ajaxSaveDDSettings']);
        $this->registerAjax('instinctor_get_billing_data', [$this, 'ajaxGetBillingData']);
        $this->registerAjax('instinctor_save_stripe_plans', [$this, 'ajaxSaveStripePlans']);
        $this->registerAjax('instinctor_save_billing_urls', [$this, 'ajaxSaveBillingUrls']);

        // Product Bundles
        $this->registerAjax('instinctor_get_bundles', [$this, 'ajaxGetBundles']);
        $this->registerAjax('instinctor_save_bundle', [$this, 'ajaxSaveBundle']);
        $this->registerAjax('instinctor_delete_bundle', [$this, 'ajaxDeleteBundle']);

        // Product Add-Ons
        $this->registerAjax('instinctor_get_addons', [$this, 'ajaxGetProductAddons']);
        $this->registerAjax('instinctor_save_addon', [$this, 'ajaxSaveProductAddon']);
        $this->registerAjax('instinctor_delete_addon', [$this, 'ajaxDeleteProductAddon']);

        // Volume Discounts
        $this->registerAjax('instinctor_get_volume_tiers', [$this, 'ajaxGetVolumeTiers']);
        $this->registerAjax('instinctor_save_volume_tier', [$this, 'ajaxSaveVolumeTier']);
        $this->registerAjax('instinctor_delete_volume_tier', [$this, 'ajaxDeleteVolumeTier']);

        // Localization: language management

        // Localization: currency management (handlers already existed, just registering get_currencies)
        $this->registerAjax('instinctor_get_currencies', [$this, 'ajaxGetCurrencies']);

        // T10 — API credentials panel (ILM absorption: SendGrid, Twilio, OpenAI, Stripe)
        $this->registerAjax('instinctor_get_credentials', [$this, 'ajaxGetCredentials']);
        $this->registerAjax('instinctor_save_credentials', [$this, 'ajaxSaveCredentials']);
    }

    public function maybeInstallRoles(): void
    {
        $installed_version = Config::get('instinctor_roles_version', '');
        if ($installed_version !== INSTINCTOR_VERSION) {
            \Instinctor\Core\RoleManager::install();
            Config::set('instinctor_roles_version', INSTINCTOR_VERSION);
        }
    }

    public function registerMenus(): void
    {
        // Ensure administrator always has required caps (not conditional on version)
        $admin = get_role('administrator');
        if ($admin) {
            foreach (['instinctor_access', 'instinctor_impulse', 'instinctor_chat', 'instinctor_store', 'instinctor_settings'] as $cap) {
                if (!$admin->has_cap($cap)) {
                    $admin->add_cap($cap, true);
                }
            }
        }
        
        // Single menu item - Instinctor workspace system
        // Icon is handled via CSS mask in instinctor-admin-menu-icon.css for proper hover/active states
        add_menu_page(
            __('Instinctor', 'instinctor'),
            __('Instinctor', 'instinctor'),
            'instinctor_access',
            'instinctor',
            [$this, 'renderImpulse'],
            'none',
            55
        );

        // Remove the duplicate submenu item that WordPress creates
        add_submenu_page(
            'instinctor',
            __('Instinctor', 'instinctor'),
            __('Instinctor', 'instinctor'),
            'instinctor_access',
            'instinctor',
            [$this, 'renderImpulse']
        );

        // Hidden: Impulse Preview (for live preview iframe)
        add_submenu_page(
            null,
            __('Preview', 'instinctor'),
            __('Preview', 'instinctor'),
            'instinctor_impulse',
            'instinctor-impulse-preview',
            [$this, 'renderImpulsePreview']
        );
    }

    public function enqueueAssets(string $hook): void
    {
        // Load menu CSS on ALL admin pages for proper menu highlighting
        wp_enqueue_style(
            'instinctor-admin-menu',
            INSTINCTOR_PLUGIN_URL . 'assets/css/admin-menu.css',
            [],
            INSTINCTOR_VERSION
        );
        
        // Only load full assets on our pages
        if (strpos($hook, 'instinctor') === false) {
            return;
        }

        // Enqueue WordPress media uploader
        wp_enqueue_media();
        
        // Force jQuery to load in header — our view outputs inline <script> tags
        // that need jQuery before wp_footer() runs (WP 6.7+ footer-loads jQuery by default)
        wp_enqueue_script('jquery');
        $wp_scripts = wp_scripts();
        // set_group(handle, recursion, group) — group 0 = header, 1 = footer
        // This is the correct WP 6.7+ API; ->args=null no longer works
        foreach (['jquery', 'jquery-core', 'jquery-migrate'] as $handle) {
            if (isset($wp_scripts->registered[$handle])) {
                $wp_scripts->set_group($handle, false, 0);
            }
        }
        
        // Load Sortable.js for drag and drop
        wp_enqueue_script(
            'sortablejs',
            INSTINCTOR_PLUGIN_URL . 'assets/js/vendor/sortable.min.js',
            [],
            '1.15.0',
            true
        );

        // Load workspace CSS
        wp_enqueue_style(
            'instinctor-workspaces',
            INSTINCTOR_PLUGIN_URL . 'assets/css/instinctor.css',
            [],
            INSTINCTOR_VERSION
        );

        wp_enqueue_style(
            'instinctor-admin-skin',
            INSTINCTOR_PLUGIN_URL . 'assets/css/instinctor-admin.css',
            ['instinctor-workspaces'],
            INSTINCTOR_VERSION
        );

        // Self-contained QR encoder — dependency of admin.js (editor "open on
        // phone" popover + Settings review-tool preview render client-side).
        wp_enqueue_script(
            'instinctor-qr',
            INSTINCTOR_PLUGIN_URL . 'assets/js/instinctor-qr.js',
            [],
            INSTINCTOR_VERSION,
            true
        );

        wp_enqueue_script(
            'instinctor-admin',
            INSTINCTOR_PLUGIN_URL . 'assets/js/admin.js',
            ['jquery', 'wp-api-fetch', 'jquery-ui-sortable', 'instinctor-qr'],
            INSTINCTOR_VERSION,
            true
        );

        wp_localize_script('instinctor-admin', 'instinctorAdmin', [
            'apiUrl'      => \Instinctor\Core\Config::restUrl('instinctor/v1/'),
            'adminUrl'    => admin_url('admin.php'),
            'nonce'       => Session::token('wp_rest'),
            'adminNonce'  => Session::token('instinctor_admin'),
            'currency'    => Config::get('instinctor_currency', 'USD'),
            'storeType'   => Config::get('instinctor_store_type', 'mixed'),
            'env'         => 'wordpress',
            'currentUser' => function_exists('wp_get_current_user') ? (function() {
                $u = wp_get_current_user();
                return $u && $u->ID ? [
                    'id'    => $u->ID,
                    'email' => $u->user_email,
                    'name'  => $u->display_name,
                    'role'  => in_array('administrator', (array) $u->roles) ? 'admin' : ($u->roles[0] ?? 'agent'),
                ] : null;
            })() : null,
        ]);
    }

    public function renderDashboard(): void
    {
        $this->renderPage('dashboard');
    }

    public function renderOrders(): void
    {
        $this->renderPage('orders');
    }

    public function renderProducts(): void
    {
        $this->renderPage('products');
    }

    public function renderDesign(): void
    {
        $this->renderPage('card-designer');
    }

    public function renderImpulse(): void
    {
        $this->renderPage('instinctor');
        $this->renderOnboarding();
    }

    /**
     * First-run welcome overlay for a new DIY (WordPress) install. A light
     * orientation shown once, until instinctor_onboarded is set: it points the
     * owner to Settings to set up their store, and does not force a store-type
     * choice at the door (that lives in Settings, where gated types read as an
     * upsell rather than a wall on the very first screen). Standalone installs
     * use SetupController instead.
     */
    public function renderOnboarding(): void
    {
        if (defined('INSTINCTOR_ENV') && INSTINCTOR_ENV === 'standalone') return;
        if (Config::get('instinctor_onboarded', false)) return;
        if (!Auth::can('manage_options')) return;
        $nonce = Session::token('instinctor_admin');
        $ov   = 'position:fixed;inset:0;z-index:100000;background:rgba(15,17,21,0.72);display:flex;align-items:center;justify-content:center;padding:24px';
        $card = 'background:#fff;border-radius:12px;max-width:480px;width:100%;padding:32px;box-shadow:0 20px 60px rgba(0,0,0,0.35);font-family:Arial,Helvetica,sans-serif';
        echo '<div id="instinctor-onboarding" data-nonce="' . esc_attr($nonce) . '" style="' . esc_attr($ov) . '"><div style="' . esc_attr($card) . '">';
        echo '<div style="font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#999;margin-bottom:8px">Welcome to Instinctor</div>';
        echo '<h2 style="font-size:24px;font-weight:800;color:#1d2327;margin:0 0 10px;letter-spacing:-0.02em">Let\'s set up your store</h2>';
        echo '<p style="font-size:14px;line-height:1.55;color:#555;margin:0 0 24px">Start in Settings: choose your store type and add your business details. Then you are ready to build your first page.</p>';
        echo '<button type="button" id="instinctor-onboarding-go" style="width:100%;background:#1d2327;color:#fff;border:0;border-radius:6px;padding:13px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit">Get started</button>';
        echo '</div></div>';
    }

    public function ajaxCompleteOnboarding(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error(['message' => 'Permission denied']);
        Config::set('instinctor_onboarded', true);
        wp_send_json_success([]);
    }

    public function renderCustomers(): void
    {
        $this->renderPage('customers');
    }

    public function renderCheckout(): void
    {
        // Checkout builder has been moved to Impulse page builder
        wp_safe_redirect(admin_url('admin.php?page=instinctor'));
        exit;
    }

    public function renderCategories(): void
    {
        $this->renderPage('categories');
    }

    public function renderCoupons(): void
    {
        $this->renderPage('coupons');
    }

    public function renderShipping(): void
    {
        $this->renderPage('shipping');
    }

    public function renderTax(): void
    {
        $this->renderPage('tax');
    }

    public function renderCurrency(): void
    {
        $this->renderPage('currency');
    }

    public function renderEmails(): void
    {
        $this->renderPage('emails');
    }

    public function renderReports(): void
    {
        $this->renderPage('reports');
    }

    public function renderBookings(): void
    {
        $this->renderPage('bookings');
    }

    public function renderSubscriptions(): void
    {
        $this->renderPage('subscriptions');
    }

    public function renderMemberships(): void
    {
        $this->renderPage('memberships');
    }

    public function renderBundles(): void
    {
        $this->renderPage('bundles');
    }

    public function renderGifts(): void
    {
        $this->renderPage('gifts');
    }

    public function renderSettings(): void
    {
        $this->renderPage('settings');
    }

    public function renderGiftCards(): void
    {
        $this->renderPage('gift-cards');
    }

    public function renderLoyalty(): void
    {
        $this->renderPage('loyalty');
    }

    public function renderInventory(): void
    {
        $this->renderPage('inventory');
    }

    public function renderTracking(): void
    {
        $this->renderPage('tracking');
    }

    public function renderRMA(): void
    {
        $this->renderPage('rma');
    }

    public function renderQA(): void
    {
        $this->renderPage('qa');
    }

    public function renderSEO(): void
    {
        $this->renderPage('seo');
    }

    public function renderCommissions(): void
    {
        $this->renderPage('commissions');
    }

    public function renderWholesale(): void
    {
    }

    public function renderAnalytics(): void
    {
        $this->renderPage('analytics');
    }


    public function renderAIRecommendations(): void
    {
        $this->renderPage('ai-recommendations');
    }

    public function renderMarketing(): void
    {
        $this->renderPage('marketing');
    }

    public function renderFinance(): void
    {
        $this->renderPage('finance');
    }

    public function renderAIConversion(): void
    {
        $this->renderPage('ai-conversion');
    }

    public function renderCheckoutUpsells(): void
    {
        $this->renderPage('checkout-upsells');
    }

    public function renderAddons(): void
    {
        // Handle integrity reset request
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- capability-gated (manage_options) maintenance switch read from a GET flag.
        if (isset($_GET['reset_integrity']) && $_GET['reset_integrity'] === '1' && Auth::can('manage_options')) {
            // TierManager reset after plugin update
            \Instinctor\Core\TierManager::reset();
            wp_safe_redirect(admin_url('admin.php?page=instinctor&integrity_reset=1'));
            exit;
        }
        
        $this->renderPage('addons');
    }

    /**
     * Handle Impulse template form actions before any output
     */
    public function handleTemplateFormActions(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below
        if (!isset($_POST['instinctor_template_action'])) {
            return;
        }
        
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'instinctor_template_editor')) {
            return;
        }
        
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $post_action = sanitize_text_field(wp_unslash((string)($_POST['instinctor_template_action'] ?? '')));
        $template_id = isset($_GET['template_id']) ? intval($_GET['template_id']) : 0;
        
        // Create tables if needed
        \Instinctor\Services\TemplateService::createTables();
        
        if ($post_action === 'create') {
            $id = \Instinctor\Services\TemplateService::saveTemplate([
                'name' => sanitize_text_field(wp_unslash($_POST['template_name'] ?? 'Untitled')),
                'type' => sanitize_text_field(wp_unslash($_POST['template_type'] ?? 'page')),
                'status' => 'draft',
            ]);
            wp_safe_redirect(admin_url('admin.php?page=instinctor&action=edit&template_id=' . $id));
            exit;
        }
        
        if ($post_action === 'delete' && $template_id) {
            \Instinctor\Services\TemplateService::deleteTemplate($template_id);
            wp_safe_redirect(admin_url('admin.php?page=instinctor&deleted=1'));
            exit;
        }
        
        if ($post_action === 'duplicate' && $template_id) {
            $newId = \Instinctor\Services\TemplateService::duplicateTemplate($template_id);
            wp_safe_redirect(admin_url('admin.php?page=instinctor&action=edit&template_id=' . $newId));
            exit;
        }
        
        if ($post_action === 'apply_starter' && $template_id) {
            $starter = sanitize_text_field(wp_unslash($_POST['starter_template'] ?? ''));
            if ($starter) {
                \Instinctor\Services\TemplateService::applyStarterTemplate($template_id, $starter);
            }
            wp_safe_redirect(admin_url('admin.php?page=instinctor&action=edit&template_id=' . $template_id . '&applied=1'));
            exit;
        }
    }

    private function renderPage(string $page): void
    {
        $template = INSTINCTOR_PLUGIN_DIR . "admin/views/{$page}.php";
        
        if (file_exists($template)) {
            ob_start();
            include $template;
            $html = ob_get_clean();
            // Strip HTML comments
            $html = preg_replace('/<!--(?!\[if)[\s\S]*?-->/', '', $html);
            // $html is the fully-rendered admin view template (plugin-generated
            // markup, escaped where each dynamic value is built). It legitimately
            // contains form controls (input/select/button/form) and data-*
            // attributes that wp_kses_post would strip and break the admin UI, so
            // escaping stays at the source and the assembled markup is emitted verbatim.
            echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        } else {
            // Fallback: React app mount point
            echo '<div class="wrap"><div id="instinctor-admin-app" data-page="' . esc_attr($page) . '"></div></div>';
        }
    }

    public function ajaxGenerateVariations(): void
    {
        check_ajax_referer('instinctor_variations', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $productId = intval($_POST['product_id'] ?? 0);
        $attributes = json_decode(sanitize_textarea_field(wp_unslash($_POST['attributes'] ?? '{}')), true);
        $basePrice = floatval($_POST['base_price'] ?? 0);

        if (!$productId || empty($attributes)) {
            wp_send_json_error('Invalid data');
        }

        $variationService = new \Instinctor\Services\VariationService();
        
        // Delete existing variations first
        $variationService->deleteProductVariations($productId);
        
        // Generate new variations
        $count = $variationService->generateVariations($productId, $attributes, $basePrice);

        wp_send_json_success(['count' => $count]);
    }

    public function ajaxSaveVariations(): void
    {
        check_ajax_referer('instinctor_variations', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $variations = json_decode(sanitize_textarea_field(wp_unslash($_POST['variations'] ?? '[]')), true);

        if (empty($variations)) {
            wp_send_json_error('No variations to save');
        }

        $variationService = new \Instinctor\Services\VariationService();

        foreach ($variations as $v) {
            $variationService->updateVariation((int) $v['id'], [
                'sku' => $v['sku'] ?? '',
                'price' => !empty($v['price']) ? (float) $v['price'] : null,
                'stock_quantity' => isset($v['stock_quantity']) && $v['stock_quantity'] !== '' ? (int) $v['stock_quantity'] : null,
            ]);
        }

        wp_send_json_success();
    }

    public function ajaxDeleteVariation(): void
    {
        check_ajax_referer('instinctor_variations', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $variationId = intval($_POST['variation_id'] ?? 0);

        if (!$variationId) {
            wp_send_json_error('Invalid variation');
        }

        $variationService = new \Instinctor\Services\VariationService();
        $variationService->deleteVariation($variationId);

        wp_send_json_success();
    }

    public function ajaxAddOrderNote(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $orderId = intval($_POST['order_id'] ?? 0);
        $note = sanitize_textarea_field($_POST['note'] ?? '');
        $isCustomerNote = !empty($_POST['is_customer_note']);

        if (!$orderId || !$note) {
            wp_send_json_error('Missing required fields');
        }

        $noteId = \Instinctor\Services\OrderNoteService::addNote($orderId, $note, $isCustomerNote, $isCustomerNote);

        wp_send_json_success(['note_id' => $noteId]);
    }

    public function ajaxDeleteOrderNote(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $noteId = intval($_POST['note_id'] ?? 0);

        if (!$noteId) {
            wp_send_json_error('Invalid note');
        }

        \Instinctor\Services\OrderNoteService::deleteNote($noteId);
        wp_send_json_success();
    }

    public function ajaxDeleteOrder(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $orderId = intval($_POST['order_id'] ?? 0);
        if (!$orderId) {
            wp_send_json_error('Invalid order');
        }

        $p = DB::prefix();
        // Delete related rows first
        DB::delete($p . 'instinctor_order_items',    ['order_id' => $orderId]);
        DB::delete($p . 'instinctor_order_notes',    ['order_id' => $orderId]);
        DB::delete($p . 'instinctor_order_tracking', ['order_id' => $orderId]);
        DB::delete($p . 'instinctor_orders',         ['id'       => $orderId]);

        wp_send_json_success();
    }

    public function ajaxDeleteClient(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $customerId = intval($_POST['customer_id'] ?? 0);
        if (!$customerId) {
            wp_send_json_error('Invalid customer');
        }

        $p = DB::prefix();
        DB::delete($p . 'instinctor_customers', ['id' => $customerId]);

        wp_send_json_success();
    }

    public function ajaxGetOrderDetail(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        

        $p = DB::prefix();
        $orderId = intval($_POST['order_id'] ?? 0);
        if (!$orderId) {
            wp_send_json_error('Invalid order');
        }

        $order = DB::get_row(DB::prepare(
            "SELECT * FROM {$p}instinctor_orders WHERE id = %d",
            $orderId
        ));

        if (!$order) {
            wp_send_json_error('Order not found');
        }

        $items = DB::get_results(DB::prepare(
            "SELECT * FROM {$p}instinctor_order_items WHERE order_id = %d",
            $orderId
        ));

        $billing = json_decode($order->billing_address ?? '{}', true);
        $customerName = trim(($billing['first_name'] ?? '') . ' ' . ($billing['last_name'] ?? ''));
        
        // Get customer email: customer table, fallback to billing_address JSON
        $customerEmail = '';
        if ($order->customer_id) {
            $customerEmail = DB::get_var(DB::prepare(
                "SELECT email FROM {$p}instinctor_customers WHERE id = %d",
                $order->customer_id
            )) ?: '';
        }
        if (empty($customerEmail)) {
            $customerEmail = $billing['email'] ?? '';
        }
        
        // Get tracking info from tracking table (primary) with meta fallback
        $trackingRow = DB::get_row(DB::prepare(
            "SELECT carrier, tracking_number, tracking_url FROM {$p}instinctor_order_tracking WHERE order_id = %d ORDER BY created_at DESC LIMIT 1",
            $orderId
        ));
        
        if ($trackingRow && $trackingRow->tracking_number) {
            $trackingNumber = $trackingRow->tracking_number;
            $trackingUrl = $trackingRow->tracking_url ?? '';
            $trackingCarrier = $trackingRow->carrier ?? 'other';
        } else {
            // Fallback to meta for legacy data
            $meta = json_decode($order->meta ?? '{}', true);
            $trackingNumber = $meta['tracking_number'] ?? '';
            $trackingUrl = $meta['tracking_url'] ?? '';
            $trackingCarrier = $meta['tracking_carrier'] ?? 'other';
        }

        wp_send_json_success([
            'id' => $order->id,
            'order_number' => $order->order_number,
            'status' => $order->status,
            'subtotal' => $order->subtotal,
            'discount_total' => $order->discount_total,
            'shipping_total' => $order->shipping_total,
            'tax_total' => $order->tax_total,
            'total' => $order->total,
            
            'currency' => Config::get('instinctor_currency_symbol', '$'),
            'customer_id' => $order->customer_id ? (int)$order->customer_id : null,
            'customer_name' => $customerName ?: null,
            'customer_email' => $customerEmail,
            'date' => date_i18n('F j, Y g:i a', strtotime($order->created_at)),
            
            'tracking_number' => $trackingNumber,
            'tracking_url' => $trackingUrl,
            'tracking_carrier' => $trackingCarrier,
            'items' => array_map(function($item) {
                $meta = json_decode($item->meta ?: '{}', true);
                return [
                    'name'     => $item->name,
                    'quantity' => $item->quantity,
                    'price'    => $item->price,
                    'total'    => $item->total,
                    'addons'   => $meta['addons'] ?? [],
                ];
            }, $items)
        ]);
    }

    public function ajaxGetCustomerDetail(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        

        $p = DB::prefix();
        $customerId = intval($_POST['customer_id'] ?? 0);
        if (!$customerId) {
            wp_send_json_error('Invalid customer');
        }

        $customer = DB::get_row(DB::prepare(
            "SELECT * FROM {$p}instinctor_customers WHERE id = %d",
            $customerId
        ));

        if (!$customer) {
            wp_send_json_error('Customer not found');
        }

        $currency = Config::get('instinctor_currency_symbol', '$');

        $orders = DB::get_results(DB::prepare(
            "SELECT id, order_number, status, total, created_at
             FROM {$p}instinctor_orders
             WHERE customer_id = %d
             ORDER BY created_at DESC
             LIMIT 50",
            $customerId
        ));

        $orderCount  = count($orders);
        $totalSpent  = 0;
        $firstDate   = null;
        $lastDate    = null;
        foreach ($orders as $o) {
            if (in_array($o->status, ['completed', 'processing'])) {
                $totalSpent += floatval($o->total);
            }
            if (!$firstDate || $o->created_at < $firstDate) $firstDate = $o->created_at;
            if (!$lastDate  || $o->created_at > $lastDate)  $lastDate  = $o->created_at;
        }

        $billing = json_decode($customer->billing_address ?? '{}', true) ?: [];
        $name    = trim(($customer->first_name ?: '') . ' ' . ($customer->last_name ?: ''));

        // Loyalty points
        $pt = DB::prefix() . 'instinctor_loyalty_points';
        $pt_exists = DB::get_var("SHOW TABLES LIKE '$pt'") === $pt;
        $loyalty = $pt_exists
            ? DB::get_row(DB::prepare("SELECT points, lifetime_points, tier FROM $pt WHERE customer_id = %d", $customerId))
            : null;

        // Gift card balance by recipient email
        $gc = DB::prefix() . 'instinctor_gift_cards';
        $gc_exists = DB::get_var("SHOW TABLES LIKE '$gc'") === $gc;
        $giftBalance = ($gc_exists && $customer->email)
            ? floatval(DB::get_var(DB::prepare("SELECT COALESCE(SUM(current_balance),0) FROM $gc WHERE recipient_email = %s AND status = 'active'", $customer->email)))
            : 0.0;

        // Memberships: surface the member's subscription(s) so the detail panel
        // shows plan/status/billing instead of the (empty) order history.
        $subscription  = null;
        $subscriptions = [];
        if (\Instinctor\Core\StoreFeatures::getStoreType() === 'memberships') {
            $subRows = DB::get_results(DB::prepare(
                "SELECT s.id, s.status, s.price, s.period, s.current_period_end,
                        s.canceled_at, s.created_at, s.payment_method, s.external_id, p.name AS plan_name
                 FROM {$p}instinctor_subscriptions s
                 LEFT JOIN {$p}instinctor_products p ON p.id = s.product_id
                 WHERE s.customer_id = %d
                 ORDER BY (s.status = 'active') DESC, s.created_at DESC",
                $customerId
            ));
            $statusLabels = [
                'active' => 'Active', 'canceled' => 'Cancelled', 'cancelled' => 'Cancelled',
                'past_due' => 'Past due', 'paused' => 'Paused', 'trialing' => 'Trial',
            ];
            foreach ($subRows as $s) {
                $per   = ((string)$s->period === 'year') ? '/yr' : '/mo';
                $price = (float)$s->price;
                $subscriptions[] = [
                    'id'           => (int)$s->id,
                    'status'       => $s->status,
                    'status_label' => $statusLabels[$s->status] ?? ucfirst(str_replace('_', ' ', (string)$s->status)),
                    'plan'         => $s->plan_name,
                    'price'        => $price,
                    'payment_method' => (string) $s->payment_method,
                    'billable'     => ($s->payment_method === 'authorizenet' && !empty($s->external_id)),
                    'price_label'  => $price > 0 ? $currency . number_format($price, ($price == floor($price) ? 0 : 2)) . $per : '',
                    'since'        => $s->created_at ? date_i18n('M j, Y', strtotime($s->created_at)) : null,
                    'renews'       => ($s->status === 'active' && $s->current_period_end) ? date_i18n('M j, Y', strtotime($s->current_period_end)) : null,
                    'canceled'     => $s->canceled_at ? date_i18n('M j, Y', strtotime($s->canceled_at)) : null,
                ];
            }
            $subscription = $subscriptions[0] ?? null;
        }

        // Salon CRM block: appointment timeline + before/after photos + color
        // formulas + soft fields. Salon vertical only, so other stores are unchanged.
        $salon = null;
        if (\Instinctor\Core\StoreFeatures::isSalon()) {
            if (!\Instinctor\Core\TierManager::hasActiveLicense()) {
                $salon = ['locked' => true];   // Pro feature — admin shows an upgrade panel
            } else {
                $prof = \Instinctor\Services\ClientService::getProfile($customerId);
                if ($prof) {
                    $salon = [
                        'visits'    => array_values(array_filter($prof['visits'], fn($v) => $v['kind'] === 'appointment')),
                        'photos'    => $prof['photos'],
                        'formulas'  => $prof['formulas'],
                        'tags'      => $prof['tags'],
                        'birthday'  => $prof['birthday'],
                        'allergies' => $prof['allergies'],
                    ];
                }
            }
        }

        wp_send_json_success([
            'id'             => (int)$customer->id,
            'salon'          => $salon,
            'name'           => $name ?: null,
            'first_name'     => (string) ($customer->first_name ?? ''),
            'last_name'      => (string) ($customer->last_name ?? ''),
            'email'          => $customer->email,
            'phone'          => $customer->phone ?: null,
            'address'        => trim(implode(', ', array_filter([
                $billing['address'] ?? $billing['address_1'] ?? '',
                $billing['city'] ?? '',
                $billing['state'] ?? '',
                $billing['country'] ?? '',
            ]))),
            'order_count'    => $orderCount,
            'total_spent'    => $totalSpent,
            'currency'       => $currency,
            'first_order'    => $firstDate ? date_i18n('M j, Y', strtotime($firstDate)) : null,
            'last_order'     => $lastDate  ? date_i18n('M j, Y', strtotime($lastDate))  : null,
            'member_since'   => date_i18n('M j, Y', strtotime($customer->created_at)),
            'subscription'   => $subscription,
            'subscriptions'  => $subscriptions,
            'loyalty_points' => $loyalty ? (int)$loyalty->points : null,
            'loyalty_tier'   => $loyalty ? ($loyalty->tier ?: 'bronze') : null,
            'gift_balance'   => $giftBalance > 0 ? $giftBalance : null,
            'orders'         => array_map(function($o) use ($currency) {
                return [
                    'id'           => (int)$o->id,
                    'order_number' => $o->order_number,
                    'status'       => $o->status,
                    'total'        => floatval($o->total),
                    'date'         => date_i18n('M j, Y', strtotime($o->created_at)),
                ];
            }, $orders),
        ]);
    }

    /**
     * Edit a member: name (first/last) and, optionally, the recurring amount.
     * The amount change is pushed to the payment gateway so future charges use
     * the new figure (Authorize.net ARB via ARBUpdateSubscription), then the
     * local subscription row is synced. The name always saves even if the
     * billing update is skipped/unsupported; any billing issue is returned as a
     * non-fatal warning so the name edit is never lost. Memberships store only.
     */
    public function ajaxUpdateMember(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error(['message' => 'Permission denied']); }

        $p          = DB::prefix();
        $customerId = (int) ($_POST['customer_id'] ?? 0);
        $first      = trim(sanitize_text_field(wp_unslash($_POST['first_name'] ?? '')));
        $last       = trim(sanitize_text_field(wp_unslash($_POST['last_name'] ?? '')));
        if (!$customerId) { wp_send_json_error(['message' => 'Invalid member']); }

        $cust = DB::get_row(DB::prepare("SELECT id FROM {$p}instinctor_customers WHERE id = %d", $customerId));
        if (!$cust) { wp_send_json_error(['message' => 'Member not found']); }

        // 1. Name (always).
        DB::update("{$p}instinctor_customers", ['first_name' => $first, 'last_name' => $last], ['id' => $customerId]);

        $out = ['name' => trim($first . ' ' . $last), 'warning' => null];

        // 2. Optional recurring amount change → gateway + local sync.
        $amountRaw = $_POST['amount'] ?? '';
        if ($amountRaw !== '' && is_numeric($amountRaw)) {
            $amount = round((float) $amountRaw, 2);
            $sub = DB::get_row(DB::prepare(
                "SELECT id, price, payment_method, external_id FROM {$p}instinctor_subscriptions
                 WHERE customer_id = %d ORDER BY (status='active') DESC, created_at DESC LIMIT 1",
                $customerId
            ));
            if ($amount <= 0) {
                $out['warning'] = 'Amount must be greater than zero; billing unchanged.';
            } elseif (!$sub) {
                $out['warning'] = 'No membership to bill; name saved.';
            } elseif ((float) $sub->price === $amount) {
                // no change to the amount — nothing to do
            } else {
                $pm = (string) $sub->payment_method;
                if ($pm === 'authorizenet' && !empty($sub->external_id)) {
                    $r = \Instinctor\Services\SubscriptionService::instance()
                        ->updateAuthorizeNetAmount((string) $sub->external_id, $amount);
                    if (empty($r['success'])) {
                        wp_send_json_error(['message' => 'Name saved, but the billing update failed: ' . ($r['error'] ?? 'unknown error') . '. The recurring amount was NOT changed.']);
                    }
                    DB::update("{$p}instinctor_subscriptions", ['price' => $amount], ['id' => (int) $sub->id]);
                    $out['amount'] = $amount;
                } elseif ($pm === 'stripe') {
                    $out['warning'] = 'Name saved. Changing a Stripe amount is not supported here yet; adjust it in Stripe.';
                } else {
                    // local/manual subscription — just sync the record
                    DB::update("{$p}instinctor_subscriptions", ['price' => $amount], ['id' => (int) $sub->id]);
                    $out['amount'] = $amount;
                }
            }
        }

        wp_send_json_success($out);
    }

    /**
     * AJAX: Get visitor note for a client record (keyed by email)
     */
    public function ajaxGetClientNote(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }

        $customerId = intval($_POST['customer_id'] ?? 0);
        if (!$customerId) { wp_send_json_error('Invalid customer'); return; }

        $p = DB::prefix();
        $email = DB::get_var(DB::prepare("SELECT email FROM {$p}instinctor_customers WHERE id = %d", $customerId));
        if (!$email) { wp_send_json_success(['note' => '']); return; }

        $nt = $p . 'instinctor_visitor_notes';
        $tableExists = DB::get_var("SHOW TABLES LIKE '$nt'");
        if (!$tableExists) { wp_send_json_success(['note' => '']); return; }

        $row = DB::get_row(DB::prepare("SELECT note FROM $nt WHERE email = %s", $email));
        wp_send_json_success(['note' => $row ? $row->note : '']);
    }

    /**
     * AJAX: Save (upsert) visitor note for a client record
     */
    public function ajaxSaveClientNote(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }

        $customerId = intval($_POST['customer_id'] ?? 0);
        $note       = isset($_POST['note']) ? sanitize_textarea_field(wp_unslash($_POST['note'])) : '';
        if (!$customerId) { wp_send_json_error('Invalid customer'); return; }

        $p = DB::prefix();
        $email = DB::get_var(DB::prepare("SELECT email FROM {$p}instinctor_customers WHERE id = %d", $customerId));
        if (!$email) { wp_send_json_error('Customer email not found'); return; }

        $nt  = $p . 'instinctor_visitor_notes';
        $now = current_time('mysql');
        $uid = \Instinctor\Core\Auth::userId();

        // Ensure table exists
        if (!DB::get_var("SHOW TABLES LIKE '$nt'")) {
            \Instinctor\Services\ChatService::instance()->createTables();
        }

        $existing = DB::get_row(DB::prepare("SELECT id FROM $nt WHERE email = %s", $email));
        if ($existing) {
            DB::update($nt, ['note' => $note, 'updated_at' => $now, 'updated_by' => $uid], ['id' => $existing->id]);
        } else {
            DB::insert($nt, ['email' => $email, 'phone' => '', 'note' => $note, 'updated_at' => $now, 'updated_by' => $uid]);
        }

        wp_send_json_success(['note' => $note]);
    }

    // ── Salon CRM: before/after photos + color formulas + soft fields ─────────
    public function ajaxClientPhotoAdd(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        if (!\Instinctor\Core\TierManager::hasActiveLicense()) { wp_send_json_error('Client profiles are a Pro feature'); return; }
        $cid = intval($_POST['customer_id'] ?? 0);
        if (!$cid) { wp_send_json_error('Invalid client'); return; }
        $before = esc_url_raw(wp_unslash($_POST['before_url'] ?? ''));
        $after  = esc_url_raw(wp_unslash($_POST['after_url'] ?? ''));
        if ($before === '' && $after === '') { wp_send_json_error('Add at least one photo'); return; }
        \Instinctor\Services\ClientService::addPhoto($cid, [
            'service'      => sanitize_text_field(wp_unslash($_POST['service'] ?? '')),
            'before_url'   => $before,
            'after_url'    => $after,
            'before_thumb' => esc_url_raw(wp_unslash($_POST['before_thumb'] ?? '')) ?: $before,
            'after_thumb'  => esc_url_raw(wp_unslash($_POST['after_thumb'] ?? '')) ?: $after,
            'taken_at'     => sanitize_text_field(wp_unslash($_POST['taken_at'] ?? '')),
            'note'         => sanitize_textarea_field(wp_unslash($_POST['note'] ?? '')),
            'booking_id'   => intval($_POST['booking_id'] ?? 0),
        ]);
        wp_send_json_success(['photos' => \Instinctor\Services\ClientService::listPhotos($cid)]);
    }

    public function ajaxClientPhotoDelete(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        if (!\Instinctor\Core\TierManager::hasActiveLicense()) { wp_send_json_error('Client profiles are a Pro feature'); return; }
        $pid = intval($_POST['photo_id'] ?? 0);
        $cid = intval($_POST['customer_id'] ?? 0);
        if (!$pid) { wp_send_json_error('Invalid photo'); return; }
        \Instinctor\Services\ClientService::deletePhoto($pid);
        wp_send_json_success(['photos' => $cid ? \Instinctor\Services\ClientService::listPhotos($cid) : []]);
    }

    public function ajaxClientFormulaSave(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        if (!\Instinctor\Core\TierManager::hasActiveLicense()) { wp_send_json_error('Client profiles are a Pro feature'); return; }
        $cid = intval($_POST['customer_id'] ?? 0);
        $fid = intval($_POST['formula_id'] ?? 0);
        if (!$cid) { wp_send_json_error('Invalid client'); return; }
        $data = [
            'title'   => sanitize_text_field(wp_unslash($_POST['title'] ?? '')),
            'formula' => sanitize_textarea_field(wp_unslash($_POST['formula'] ?? '')),
            'brand'   => sanitize_text_field(wp_unslash($_POST['brand'] ?? '')),
            'service' => sanitize_text_field(wp_unslash($_POST['service'] ?? '')),
            'used_at' => sanitize_text_field(wp_unslash($_POST['used_at'] ?? '')),
        ];
        if ($fid) \Instinctor\Services\ClientService::updateFormula($fid, $data);
        else      \Instinctor\Services\ClientService::addFormula($cid, $data);
        wp_send_json_success(['formulas' => \Instinctor\Services\ClientService::listFormulas($cid)]);
    }

    public function ajaxClientFormulaDelete(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        if (!\Instinctor\Core\TierManager::hasActiveLicense()) { wp_send_json_error('Client profiles are a Pro feature'); return; }
        $fid = intval($_POST['formula_id'] ?? 0);
        $cid = intval($_POST['customer_id'] ?? 0);
        if (!$fid) { wp_send_json_error('Invalid formula'); return; }
        \Instinctor\Services\ClientService::deleteFormula($fid);
        wp_send_json_success(['formulas' => $cid ? \Instinctor\Services\ClientService::listFormulas($cid) : []]);
    }

    public function ajaxClientMetaSave(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        if (!\Instinctor\Core\TierManager::hasActiveLicense()) { wp_send_json_error('Client profiles are a Pro feature'); return; }
        $cid = intval($_POST['customer_id'] ?? 0);
        if (!$cid) { wp_send_json_error('Invalid client'); return; }
        $fields = [];
        foreach (['tags', 'birthday', 'allergies'] as $k) {
            if (isset($_POST[$k])) $fields[$k] = sanitize_text_field(wp_unslash($_POST[$k]));
        }
        \Instinctor\Services\ClientService::saveMeta($cid, $fields);
        $prof = \Instinctor\Services\ClientService::getProfile($cid);
        wp_send_json_success(['tags' => $prof['tags'] ?? [], 'birthday' => $prof['birthday'] ?? '', 'allergies' => $prof['allergies'] ?? '']);
    }

    public function ajaxGetCustomers(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');

        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }


        $segment  = sanitize_key($_POST['segment'] ?? 'all');
        $search   = sanitize_text_field($_POST['search'] ?? '');
        $page     = max(1, intval($_POST['page'] ?? 1));
        $per_page = 50;
        $offset   = ($page - 1) * $per_page;
        $cst      = DB::prefix() . 'instinctor_customers';
        $ord      = DB::prefix() . 'instinctor_orders';
        $currency = Config::get('instinctor_currency_symbol', '$');

        // Memberships stores list MEMBERS (people with a subscription), not
        // order-based clients. The order_count/total_spent/last_order metrics
        // are meaningless when revenue comes from recurring subscriptions, so
        // members are sourced from instinctor_subscriptions with their plan,
        // status, and start date instead. Account-only customers (no
        // subscription) are excluded.
        if (\Instinctor\Core\StoreFeatures::getStoreType() === 'memberships') {
            $this->getMembersList($segment, $search, $page, $per_page, $offset, $cst, $currency);
            return;
        }

        $where_parts = [];
        $where_args  = [];

        if ($search !== '') {
            $like = '%' . DB::esc_like($search) . '%';
            $where_parts[] = '(c.email LIKE %s OR c.first_name LIKE %s OR c.last_name LIKE %s OR c.phone LIKE %s)';
            array_push($where_args, $like, $like, $like, $like);
        }

        $where_sql = $where_parts ? 'WHERE ' . implode(' AND ', $where_parts) : '';

        $having_sql = '';
        $threshold  = floatval(Config::get('instinctor_high_value_threshold', 200));
        switch ($segment) {
            case 'repeat':     $having_sql = 'HAVING order_count >= 2'; break;
            case 'high_value': $having_sql = "HAVING total_spent >= {$threshold}"; break;
            case 'inactive':   $having_sql = 'HAVING last_order_date IS NULL OR last_order_date < DATE_SUB(NOW(), INTERVAL 90 DAY)'; break;
        }

        $inner_sql = "SELECT c.id, c.first_name, c.last_name, c.email, c.phone,
                        COUNT(o.id) as order_count,
                        COALESCE(SUM(CASE WHEN o.status IN ('completed','processing') THEN o.total ELSE 0 END), 0) as total_spent,
                        MAX(o.created_at) as last_order_date
                     FROM {$cst} c
                     LEFT JOIN {$ord} o ON o.customer_id = c.id
                     {$where_sql}
                     GROUP BY c.id
                     {$having_sql}";

        $list_sql  = "{$inner_sql} ORDER BY total_spent DESC, order_count DESC LIMIT %d OFFSET %d";
        $list_args = array_merge($where_args, [$per_page, $offset]);
        $customers = DB::get_results(DB::prepare($list_sql, ...$list_args));

        // Count must use same GROUP BY + HAVING to get correct total for the active segment
        $count_sql  = "SELECT COUNT(*) FROM ({$inner_sql}) AS _filtered";
        $total      = $where_args
            ? (int)DB::get_var(DB::prepare($count_sql, ...$where_args))
            : (int)DB::get_var($count_sql);

        $responseData = [
            'customers' => array_map(function($c) use ($currency) {
                $name = trim(($c->first_name ?: '') . ' ' . ($c->last_name ?: ''));
                return [
                    'id'              => (int)$c->id,
                    'name'            => $name ?: null,
                    'email'           => $c->email,
                    'phone'           => $c->phone ?: null,
                    'order_count'     => (int)$c->order_count,
                    'total_spent'     => floatval($c->total_spent),
                    'last_order_date' => $c->last_order_date
                        ? date_i18n('M j, Y', strtotime($c->last_order_date))
                        : null,
                    'currency'        => $currency,
                ];
            }, $customers),
            'total'    => $total,
            'page'     => $page,
            'has_more' => ($offset + $per_page) < $total,
        ];

        // Segment counts on first page load
        if ($page === 1) {
            $base = "SELECT c.id, COUNT(o.id) as order_count,
                        COALESCE(SUM(CASE WHEN o.status IN ('completed','processing') THEN o.total ELSE 0 END), 0) as total_spent,
                        MAX(o.created_at) as last_order_date
                     FROM {$cst} c LEFT JOIN {$ord} o ON o.customer_id = c.id GROUP BY c.id";
            $responseData['segment_counts'] = [
                'all'        => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _a"),
                'repeat'     => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _r WHERE order_count >= 2"),
                'high_value' => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _h WHERE total_spent >= {$threshold}"),
                'inactive'   => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _i WHERE last_order_date IS NULL OR last_order_date < DATE_SUB(NOW(), INTERVAL 90 DAY)"),
                'new_30d'    => (int)DB::get_var(DB::prepare("SELECT COUNT(*) FROM {$cst} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)")),
            ];
        }

        wp_send_json_success($responseData);
    }

    /**
     * Members list for memberships stores. One row per subscription-holder,
     * surfacing the primary subscription's status, plan, price, and start date.
     * The "primary" subscription is the active one if present, else the most
     * recent (resolved via GROUP_CONCAT ordered active-first, newest-first).
     */
    private function getMembersList(string $segment, string $search, int $page, int $per_page, int $offset, string $cst, string $currency): void
    {
        $subs = DB::prefix() . 'instinctor_subscriptions';
        $prd  = DB::prefix() . 'instinctor_products';

        $where_parts = [];
        $where_args  = [];
        if ($search !== '') {
            $like = '%' . DB::esc_like($search) . '%';
            $where_parts[] = '(c.email LIKE %s OR c.first_name LIKE %s OR c.last_name LIKE %s OR c.phone LIKE %s)';
            array_push($where_args, $like, $like, $like, $like);
        }
        $where_sql = $where_parts ? 'WHERE ' . implode(' AND ', $where_parts) : '';

        $order_pref = "(s.status = 'active') DESC, s.created_at DESC";
        $inner_sql = "SELECT c.id, c.first_name, c.last_name, c.email, c.phone,
                        MAX(CASE WHEN s.status = 'active' THEN 1 ELSE 0 END) AS has_active,
                        MIN(s.created_at) AS member_since,
                        SUBSTRING_INDEX(GROUP_CONCAT(s.status     ORDER BY {$order_pref}), ',', 1) AS primary_status,
                        SUBSTRING_INDEX(GROUP_CONCAT(s.price      ORDER BY {$order_pref}), ',', 1) AS primary_price,
                        SUBSTRING_INDEX(GROUP_CONCAT(s.period     ORDER BY {$order_pref}), ',', 1) AS primary_period,
                        SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(s.product_id,0) ORDER BY {$order_pref}), ',', 1) AS primary_plan_id
                     FROM {$cst} c
                     INNER JOIN {$subs} s ON s.customer_id = c.id
                     {$where_sql}
                     GROUP BY c.id";

        $having_sql = '';
        switch ($segment) {
            case 'active':   $having_sql = 'HAVING has_active = 1'; break;
            case 'canceled': $having_sql = 'HAVING has_active = 0'; break;
            case 'new':      $having_sql = 'HAVING member_since >= DATE_SUB(NOW(), INTERVAL 30 DAY)'; break;
        }

        $list_sql  = "{$inner_sql} {$having_sql} ORDER BY has_active DESC, member_since DESC LIMIT %d OFFSET %d";
        $list_args = array_merge($where_args, [$per_page, $offset]);
        $members   = DB::get_results(DB::prepare($list_sql, ...$list_args));

        // Resolve plan names in one query.
        $plan_ids = array_values(array_unique(array_filter(array_map(fn($m) => (int)$m->primary_plan_id, $members))));
        $plan_names = [];
        if ($plan_ids) {
            $in = implode(',', array_map('intval', $plan_ids));
            foreach (DB::get_results("SELECT id, name FROM {$prd} WHERE id IN ({$in})") as $p) {
                $plan_names[(int)$p->id] = $p->name;
            }
        }

        $count_sql = "SELECT COUNT(*) FROM ({$inner_sql} {$having_sql}) AS _filtered";
        $total     = $where_args
            ? (int)DB::get_var(DB::prepare($count_sql, ...$where_args))
            : (int)DB::get_var($count_sql);

        $statusLabels = [
            'active' => 'Active', 'canceled' => 'Cancelled', 'cancelled' => 'Cancelled',
            'past_due' => 'Past due', 'paused' => 'Paused', 'trialing' => 'Trial',
        ];

        $responseData = [
            'mode'      => 'memberships',
            'customers' => array_map(function($m) use ($currency, $plan_names, $statusLabels) {
                $name   = trim(($m->first_name ?: '') . ' ' . ($m->last_name ?: ''));
                $status = ((int)$m->has_active === 1) ? 'active' : ((string)$m->primary_status ?: 'canceled');
                $price  = (float)$m->primary_price;
                $per    = ((string)$m->primary_period === 'year') ? '/yr' : '/mo';
                return [
                    'id'           => (int)$m->id,
                    'name'         => $name ?: null,
                    'email'        => $m->email,
                    'phone'        => $m->phone ?: null,
                    'status'       => $status,
                    'status_label' => $statusLabels[$status] ?? ucfirst(str_replace('_', ' ', $status)),
                    'plan'         => $plan_names[(int)$m->primary_plan_id] ?? null,
                    'price'        => $price,
                    'price_label'  => $price > 0 ? $currency . number_format($price, ($price == floor($price) ? 0 : 2)) . $per : '',
                    'member_since' => $m->member_since ? date_i18n('M j, Y', strtotime($m->member_since)) : null,
                    'currency'     => $currency,
                ];
            }, $members),
            'total'    => $total,
            'page'     => $page,
            'has_more' => ($offset + $per_page) < $total,
        ];

        if ($page === 1) {
            $base = "SELECT c.id, MAX(CASE WHEN s.status='active' THEN 1 ELSE 0 END) AS has_active, MIN(s.created_at) AS member_since
                     FROM {$cst} c INNER JOIN {$subs} s ON s.customer_id = c.id GROUP BY c.id";
            $responseData['segment_counts'] = [
                'all'      => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _a"),
                'active'   => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _b WHERE has_active = 1"),
                'canceled' => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _c WHERE has_active = 0"),
                'new_30d'  => (int)DB::get_var("SELECT COUNT(*) FROM ({$base}) AS _d WHERE member_since >= DATE_SUB(NOW(), INTERVAL 30 DAY)"),
            ];
        }

        wp_send_json_success($responseData);
    }

    /**
     * Build a blog-post (article) element tree: site header, a centred white
     * content section holding an optional cover image, the title, and the
     * body, then the footer. Everything except header/footer is a text element
     * (HTML content) so there's no per-block schema to get wrong. The post is
     * rendered in flow mode, so it reads as a plain scrolling article.
     */
    private function buildArticleElements(string $title, string $bodyHtml, string $coverUrl, ?array $headerEl, ?array $footerEl): array
    {
        $els = [];
        if ($headerEl) { $headerEl['parent_id'] = null; unset($headerEl['page_id']); $els[] = $headerEl; }
        $els[] = ['id' => 'art_sec', 'type' => 'section', 'parent_id' => null, 'settings' => [
            'bg_color' => '#ffffff', 'min_height' => '', 'height_mode' => 'auto', 'valign' => 'top',
            'content_align' => 'left', 'max_content_width' => '760', 'item_space' => '18',
            'padding_top' => '56', 'padding_bottom' => '80', 'padding_left' => '24', 'padding_right' => '24',
        ]];
        if ($coverUrl !== '') {
            $els[] = ['id' => 'art_cover', 'type' => 'text', 'parent_id' => 'art_sec', 'settings' => [
                'content' => '<img src="' . esc_url($coverUrl) . '" alt="" style="width:100%;border-radius:8px;display:block">',
            ]];
        }
        $els[] = ['id' => 'art_title', 'type' => 'text', 'parent_id' => 'art_sec', 'settings' => [
            'content' => '<h1 style="font-size:36px;line-height:1.2;color:#1d2327;margin:0;text-align:left">' . esc_html($title) . '</h1>',
            'color' => '#1d2327', 'align' => 'left', 'text_align' => 'left',
        ]];
        $els[] = ['id' => 'art_body', 'type' => 'text', 'parent_id' => 'art_sec', 'settings' => [
            'content' => '<div style="text-align:left">' . ($bodyHtml !== '' ? $bodyHtml : '<p style="color:#888">Start writing…</p>') . '</div>',
            'color' => '#333333', 'align' => 'left', 'text_align' => 'left',
        ]];
        if ($footerEl) { $footerEl['parent_id'] = null; unset($footerEl['page_id']); $els[] = $footerEl; }
        return $els;
    }

    /** A clean, unique slug for a new post (no timestamp suffix). */
    private function uniquePostSlug(string $title): string
    {
        $base = sanitize_title($title);
        if ($base === '') $base = 'post';
        $p = DB::prefix();
        $slug = $base; $i = 2;
        while (DB::get_var(DB::prepare("SELECT id FROM `{$p}instinctor_pages` WHERE slug = %s LIMIT 1", $slug))) {
            $slug = $base . '-' . $i;
            $i++;
        }
        return $slug;
    }

    /** Canonical header + footer for new posts, cloned from the front page. */
    private function siteHeaderFooter(): array
    {
        $h = $f = null;
        $frontId = (int) Config::get('instinctor_front_page_id', 0);
        $src = $frontId ? PageService::getElements($frontId) : [];
        foreach ($src as $e) {
            if (($e['type'] ?? '') === 'header' && !$h) $h = $e;
            if (($e['type'] ?? '') === 'footer' && !$f) $f = $e;
        }
        return [$h, $f];
    }

    /**
     * Auto-create the /blog index page the first time a post is published.
     * It lists posts via a Posts block and renders in flow (classic, white)
     * mode, so it reads as a clean document. Element ids are NUMERIC because
     * the builder's element routes are numeric-only (a non-numeric id 404s
     * the settings panel). No-op when a /blog page already exists. Once the
     * page exists the site header adds a Blog nav link on its own. Returns
     * the blog page id, or 0 on failure.
     */
    private function ensureBlogIndexPage(): int
    {
        $existing = PageService::getByPublicSlug('blog');
        if ($existing) return (int) ($existing['id'] ?? 0);

        [$hdr, $ftr] = $this->siteHeaderFooter();

        $secId   = 1900000000001;
        $postsId = 1900000000002;

        $els = [];
        if ($hdr) { $hdr['parent_id'] = null; unset($hdr['page_id']); $els[] = $hdr; }
        $els[] = ['id' => $secId, 'type' => 'section', 'parent_id' => null, 'settings' => [
            'bg_color' => '#ffffff', 'height_mode' => 'auto', 'min_height' => '', 'valign' => 'top',
            'content_align' => 'left', 'max_content_width' => '1080', 'item_space' => '18',
            'padding_top' => '56', 'padding_bottom' => '80', 'padding_left' => '24', 'padding_right' => '24',
        ]];
        $els[] = ['id' => $postsId, 'type' => 'posts', 'parent_id' => $secId, 'settings' => [
            'heading' => 'Blog', 'columns' => 2, 'limit' => 12, 'show_date' => true, 'show_excerpt' => true,
        ]];
        if ($ftr) { $ftr['parent_id'] = null; unset($ftr['page_id']); $els[] = $ftr; }

        $blogId = (int) PageService::create([
            'name' => 'Blog', 'type' => 'page', 'status' => 'published',
            'slug' => 'blog', 'elements' => $els, 'settings' => [],
        ]);
        if (!$blogId) return 0;

        PageService::saveSettings($blogId, ['body_scroll' => 'flow', 'bg_color' => '#ffffff']);
        return $blogId;
    }

    /** Save a blog post from the simple editor (title + cover + body). */
    public function ajaxSavePost(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('edit_pages') && !Auth::can('manage_options')) { wp_send_json_error('Permission denied'); }

        $postId  = (int) ($_POST['post_id'] ?? 0);
        $title   = trim(sanitize_text_field(wp_unslash($_POST['title'] ?? '')));
        $bodyRaw = function_exists('wp_kses_post') ? wp_kses_post(wp_unslash($_POST['body'] ?? '')) : wp_unslash($_POST['body'] ?? '');
        $bodyRaw = $this->externalLinksNewTab((string) $bodyRaw);
        $cover   = esc_url_raw(wp_unslash($_POST['cover'] ?? ''));
        $excerpt = trim(sanitize_text_field(wp_unslash($_POST['excerpt'] ?? '')));
        $status  = (($_POST['status'] ?? 'draft') === 'published') ? 'published' : 'draft';

        if ($title === '') $title = 'Untitled post';
        if ($excerpt === '') {
            $plain = trim(preg_replace('/\s+/', ' ', wp_strip_all_tags((string) $bodyRaw)));
            $excerpt = mb_substr($plain, 0, 160);
        }

        [$hdr, $ftr] = $this->siteHeaderFooter();

        if ($postId > 0) {
            // Preserve the post's own header/footer if it already has them.
            $eh = $ef = null;
            foreach (PageService::getElements($postId) as $e) {
                if (($e['type'] ?? '') === 'header' && !$eh) $eh = $e;
                if (($e['type'] ?? '') === 'footer' && !$ef) $ef = $e;
            }
            PageService::saveElements($postId, $this->buildArticleElements($title, (string) $bodyRaw, $cover, $eh ?: $hdr, $ef ?: $ftr));
            $s = PageService::getSettings($postId);
        } else {
            $newId = PageService::create([
                'name' => $title, 'type' => 'post', 'status' => $status,
                'slug' => $this->uniquePostSlug($title),
                'elements' => $this->buildArticleElements($title, (string) $bodyRaw, $cover, $hdr, $ftr),
                'settings' => [],
            ]);
            if (!$newId) { wp_send_json_error('Could not create post'); }
            $postId = (int) $newId;
            $s = [];
        }

        $s['body_scroll']  = 'flow';
        $s['bg_color']     = '#ffffff';
        $s['post_excerpt'] = $excerpt;
        if (empty($s['post_date'])) $s['post_date'] = date('Y-m-d');
        PageService::saveSettings($postId, $s);

        $p = DB::prefix();
        DB::update($p . 'instinctor_pages', ['status' => $status, 'name' => $title], ['id' => $postId]);
        if ($cover !== '') PageService::saveThumbnail($postId, $cover);

        // First published post auto-creates the /blog index page; the site
        // header then surfaces a Blog nav link on its own.
        if ($status === 'published') $this->ensureBlogIndexPage();

        $slug = (string) (PageService::getById($postId)['slug'] ?? '');
        wp_send_json_success(['post_id' => $postId, 'status' => $status, 'url' => '/' . ltrim($slug, '/')]);
    }

    /** Load a post's fields back into the simple editor. */
    public function ajaxGetPost(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('edit_pages') && !Auth::can('manage_options')) { wp_send_json_error('Permission denied'); }
        $postId = (int) ($_POST['post_id'] ?? 0);
        $page = $postId ? PageService::getById($postId) : null;
        if (!$page) { wp_send_json_error('Post not found'); }
        $settings = PageService::getSettings($postId);
        $body = ''; $cover = (string) ($page['thumbnail'] ?? '');
        foreach (PageService::getElements($postId) as $e) {
            if (($e['id'] ?? '') === 'art_body') $body = (string) ($e['settings']['content'] ?? '');
        }
        wp_send_json_success([
            'post_id' => $postId,
            'title'   => (string) ($page['name'] ?? ''),
            'body'    => $body,
            'cover'   => $cover,
            'excerpt' => (string) ($settings['post_excerpt'] ?? ''),
            'status'  => (string) ($page['status'] ?? 'draft'),
        ]);
    }

    /**
     * Open external links in a new tab with rel="noopener noreferrer".
     * Blog posts are authored only by admins, so links are allowed (they're a
     * core part of a useful blog); we just sanitise (wp_kses_post already
     * strips javascript:/data: hrefs) and harden external ones. Internal
     * relative links and same-domain links stay in the same tab.
     */
    private function externalLinksNewTab(string $html): string
    {
        $host = wp_parse_url(function_exists('home_url') ? (string) home_url() : '', PHP_URL_HOST) ?: '';
        return (string) preg_replace_callback('#<a\b([^>]*)>#i', function ($m) use ($host) {
            $attrs = $m[1];
            if (!preg_match('#href\s*=\s*"(https?://[^"]+)"#i', $attrs, $h)) return $m[0]; // not an external absolute URL
            if ($host !== '' && stripos($h[1], '//' . $host) !== false) return $m[0];      // own domain — leave alone
            if (stripos($attrs, 'target=') === false) $attrs .= ' target="_blank"';
            if (stripos($attrs, 'rel=') === false)    $attrs .= ' rel="noopener noreferrer"';
            return '<a' . $attrs . '>';
        }, $html);
    }

    public function ajaxGetAllCoupons(): void
    {
        check_ajax_referer('instinctor_marketing', '_wpnonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        $couponService = new \Instinctor\Services\CouponService();
        $coupons = $couponService->getAllCoupons(['limit' => 200]);
        wp_send_json_success(['coupons' => $coupons]);
    }

    public function ajaxGetCouponDetail(): void
    {
        check_ajax_referer('instinctor_marketing', '_wpnonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $couponId = intval($_POST['coupon_id'] ?? 0);
        if (!$couponId) {
            wp_send_json_error('Invalid coupon');
        }

        $couponService = new \Instinctor\Services\CouponService();
        $coupon = $couponService->getCouponById($couponId);

        if (!$coupon) {
            wp_send_json_error('Coupon not found');
        }

        wp_send_json_success([
            'id'                   => $coupon->id,
            'code'                 => $coupon->code,
            'description'          => $coupon->description ?? '',
            'discount_type'        => $coupon->discount_type,
            'amount'               => $coupon->amount,
            'shipping'        => !empty($coupon->shipping),
            'minimum_amount'       => $coupon->minimum_amount,
            'maximum_amount'       => $coupon->maximum_amount ?? '',
            'usage_limit'          => $coupon->usage_limit,
            'usage_limit_per_user' => $coupon->usage_limit_per_user ?? '',
            'usage_count'          => $coupon->usage_count ?? 0,
            'start_date'           => $coupon->start_date ? date('Y-m-d', strtotime($coupon->start_date)) : '',
            'end_date'             => $coupon->end_date ? date('Y-m-d', strtotime($coupon->end_date)) : '',
            'status'               => $coupon->status,
        ]);
    }

    public function ajaxSaveCoupon(): void
    {
        check_ajax_referer('instinctor_marketing', '_wpnonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $couponService = new \Instinctor\Services\CouponService();
        
        $couponId = intval($_POST['coupon_id'] ?? 0);
        $code = sanitize_text_field($_POST['code'] ?? '');
        
        if (empty($code)) {
            wp_send_json_error(['message' => 'Coupon code is required']);
        }
        
        $couponData = [
            'code'                 => strtoupper($code),
            'description'          => sanitize_textarea_field($_POST['description'] ?? ''),
            'discount_type'        => sanitize_text_field($_POST['discount_type'] ?? 'percent'),
            'amount'               => floatval($_POST['amount'] ?? 0),
            'shipping'        => !empty($_POST['shipping']),
            'minimum_amount'       => !empty($_POST['minimum_amount']) ? floatval($_POST['minimum_amount']) : null,
            'maximum_amount'       => !empty($_POST['maximum_amount']) ? floatval($_POST['maximum_amount']) : null,
            'usage_limit'          => !empty($_POST['usage_limit']) ? intval($_POST['usage_limit']) : null,
            'usage_limit_per_user' => !empty($_POST['usage_limit_per_user']) ? intval($_POST['usage_limit_per_user']) : null,
            'start_date'           => !empty($_POST['start_date']) ? sanitize_text_field($_POST['start_date']) : null,
            'end_date'             => !empty($_POST['end_date']) ? sanitize_text_field($_POST['end_date']) : null,
            'status'               => sanitize_text_field($_POST['status'] ?? 'active'),
        ];

        if ($couponId) {
            $couponService->updateCoupon($couponId, $couponData);
            wp_send_json_success(['message' => 'Coupon updated']);
        } else {
            // Check if code exists
            if ($couponService->getCoupon($couponData['code'])) {
                wp_send_json_error(['message' => 'Coupon code already exists']);
            }
            $couponService->createCoupon($couponData);
            wp_send_json_success(['message' => 'Coupon created']);
        }
    }

    public function ajaxDeleteCoupon(): void
    {
        check_ajax_referer('instinctor_marketing', '_wpnonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $couponId = intval($_POST['coupon_id'] ?? 0);
        if (!$couponId) {
            wp_send_json_error('Invalid coupon');
        }

        $couponService = new \Instinctor\Services\CouponService();
        $couponService->deleteCoupon($couponId);
        
        wp_send_json_success();
    }

    public function ajaxGetGiftCards(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $status = sanitize_text_field($_POST['status'] ?? '');
        $search = sanitize_text_field($_POST['search'] ?? '');

        $args = ['limit' => 100];
        if ($status) $args['status'] = $status;
        if ($search) $args['search'] = $search;

        $cards = \Instinctor\Services\GiftCardService::getAll($args);
        $currency = Config::get('instinctor_currency_symbol', '$');

        $out = [];
        foreach ($cards as $c) {
            $out[] = [
                'id'               => $c->id,
                'code'             => $c->code,
                'initial_balance'  => $currency . number_format($c->initial_balance, 2),
                'current_balance'  => $currency . number_format($c->current_balance, 2),
                'status'           => $c->status,
                'recipient_email'  => $c->recipient_email,
                'recipient_name'   => $c->recipient_name,
                'expires_at'       => $c->expires_at ? wp_date('M j, Y', strtotime($c->expires_at)) : 'Never',
                'is_delivered'     => (bool) $c->is_delivered,
                'created_at'       => wp_date('M j, Y', strtotime($c->created_at)),
            ];
        }

        wp_send_json_success(['cards' => $out]);
    }

    public function ajaxGetGiftCardDetail(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $id = intval($_POST['id'] ?? 0);
        if (!$id) {
            wp_send_json_error('Invalid ID');
        }

        $gc = \Instinctor\Services\GiftCardService::getById($id);
        if (!$gc) {
            wp_send_json_error('Gift card not found');
        }

        $currency = Config::get('instinctor_currency_symbol', '$');
        $transactions = \Instinctor\Services\GiftCardService::getTransactions($id);

        $txOut = [];
        foreach ($transactions as $t) {
            $txOut[] = [
                'type'    => $t->type,
                'amount'  => ($t->amount >= 0 ? '+' : '') . $currency . number_format(abs($t->amount), 2),
                'note'    => $t->note,
                'date'    => wp_date('M j, Y', strtotime($t->created_at)),
            ];
        }

        wp_send_json_success([
            'id'               => $gc->id,
            'code'             => $gc->code,
            'initial_balance'  => $currency . number_format($gc->initial_balance, 2),
            'current_balance'  => $currency . number_format($gc->current_balance, 2),
            'status'           => $gc->status,
            'recipient_email'  => $gc->recipient_email,
            'recipient_name'   => $gc->recipient_name,
            'sender_name'      => $gc->sender_name,
            'message'          => $gc->message,
            'expires_at'       => $gc->expires_at ? wp_date('M j, Y', strtotime($gc->expires_at)) : 'Never',
            'is_delivered'     => (bool) $gc->is_delivered,
            'created_at'       => wp_date('M j, Y', strtotime($gc->created_at)),
            'transactions'     => $txOut,
        ]);
    }

    public function ajaxCreateGiftCard(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $card = \Instinctor\Services\GiftCardService::create([
            'amount'          => floatval($_POST['amount'] ?? 0),
            'recipient_email' => sanitize_email($_POST['recipient_email'] ?? ''),
            'recipient_name'  => sanitize_text_field($_POST['recipient_name'] ?? ''),
            'sender_name'     => sanitize_text_field($_POST['sender_name'] ?? get_bloginfo('name')),
            'message'         => sanitize_textarea_field($_POST['message'] ?? ''),
            'expires_days'    => intval($_POST['expires_days'] ?? 0),
        ]);

        if (!$card) {
            wp_send_json_error(['message' => 'Failed to create gift card — check amount is > 0']);
        }

        if (!empty($_POST['send_now']) && $card->recipient_email) {
            \Instinctor\Services\GiftCardService::sendEmail($card->id);
        }

        wp_send_json_success(['id' => $card->id, 'code' => $card->code]);
    }

    public function ajaxSendGiftCard(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id = intval($_POST['id'] ?? 0);

        if (\Instinctor\Services\GiftCardService::sendEmail($id)) {
            wp_send_json_success(['message' => 'Gift card sent']);
        } else {
            wp_send_json_error(['message' => 'Failed to send — check recipient email is set']);
        }
    }

    public function ajaxDeactivateGiftCard(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id = intval($_POST['id'] ?? 0);

        if (\Instinctor\Services\GiftCardService::updateStatus($id, 'cancelled')) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => 'Failed to deactivate']);
        }
    }

    public function ajaxAdjustLoyaltyPoints(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $customerId = intval($_POST['customer_id'] ?? 0);
        $delta      = intval($_POST['delta'] ?? 0);
        $reason     = sanitize_text_field($_POST['reason'] ?? 'Manual adjustment');

        if (!$customerId || $delta === 0) {
            wp_send_json_error('Invalid parameters');
        }

        $pt = DB::prefix() . 'instinctor_loyalty_points';
        $tt = DB::prefix() . 'instinctor_loyalty_transactions';

        // Upsert points row
        $existing = DB::get_row(DB::prepare("SELECT * FROM $pt WHERE customer_id = %d", $customerId));
        if ($existing) {
            $newPoints = max(0, (int)$existing->points + $delta);
            $newLifetime = $delta > 0 ? (int)$existing->lifetime_points + $delta : (int)$existing->lifetime_points;
            DB::update($pt, ['points' => $newPoints, 'lifetime_points' => $newLifetime], ['customer_id' => $customerId]);
        } else {
            $newPoints = max(0, $delta);
            DB::insert($pt, ['customer_id' => $customerId, 'points' => $newPoints, 'lifetime_points' => max(0, $delta)]);
        }

        // Log transaction
        DB::insert($tt, [
            'customer_id' => $customerId,
            'points'      => $delta,
            'type'        => 'adjust',
            'description' => $reason,
            'created_at'  => current_time('mysql'),
        ]);

        wp_send_json_success(['points' => $newPoints]);
    }

    public function ajaxGetLoyaltySettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $settings = [
            'enabled'            => (bool) Config::get('instinctor_loyalty_enabled', false),
            'points_per_dollar'  => floatval(Config::get('instinctor_loyalty_points_per_dollar', 1)),
            'points_value'       => floatval(Config::get('instinctor_loyalty_points_value', 0.01)),
            'min_redeem'         => intval(Config::get('instinctor_loyalty_min_redeem', 100)),
            'signup_points'      => intval(Config::get('instinctor_loyalty_signup_points', 100)),
            'referral_points'    => intval(Config::get('instinctor_loyalty_referral_points', 200)),
            'expiry_days'        => intval(Config::get('instinctor_loyalty_expiry_days', 365)),
        ];

        wp_send_json_success(['settings' => $settings]);
    }

    public function ajaxSaveLoyaltySettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');

        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        Config::set('instinctor_loyalty_enabled',         !empty($_POST['enabled']));
        Config::set('instinctor_loyalty_points_per_dollar', max(0.1, floatval($_POST['points_per_dollar'] ?? 1)));
        Config::set('instinctor_loyalty_points_value',    max(0.001, floatval($_POST['points_value'] ?? 0.01)));
        Config::set('instinctor_loyalty_min_redeem',      max(1, intval($_POST['min_redeem'] ?? 100)));
        Config::set('instinctor_loyalty_signup_points',   max(0, intval($_POST['signup_points'] ?? 100)));
        Config::set('instinctor_loyalty_referral_points', max(0, intval($_POST['referral_points'] ?? 200)));
        Config::set('instinctor_loyalty_expiry_days',     max(0, intval($_POST['expiry_days'] ?? 365)));

        wp_send_json_success(['message' => 'Settings saved']);
    }

    public function ajaxGetLoyaltyStats(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $pt = DB::prefix() . 'instinctor_loyalty_points';
        $tt = DB::prefix() . 'instinctor_loyalty_transactions';

        // Check tables exist before querying
        $pt_exists = DB::get_var("SHOW TABLES LIKE '$pt'") === $pt;
        $tt_exists = DB::get_var("SHOW TABLES LIKE '$tt'") === $tt;

        $members       = $pt_exists ? (int) DB::get_var("SELECT COUNT(*) FROM $pt") : 0;
        $active_points = $pt_exists ? (int) DB::get_var("SELECT COALESCE(SUM(points),0) FROM $pt") : 0;
        $redeemed      = $tt_exists ? (int) DB::get_var("SELECT COALESCE(ABS(SUM(points)),0) FROM $tt WHERE type = 'redeem'") : 0;

        wp_send_json_success([
            'members'       => $members,
            'active_points' => $active_points,
            'redeemed'      => $redeemed,
        ]);
    }

    public function ajaxSaveTracking(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        

        $p = DB::prefix();
        $orderId = intval($_POST['order_id'] ?? 0);
        $trackingNumber = sanitize_text_field($_POST['tracking_number'] ?? '');
        $trackingUrl = esc_url_raw($_POST['tracking_url'] ?? '');
        $carrier = sanitize_text_field($_POST['carrier'] ?? 'other');

        if (!$orderId) {
            wp_send_json_error('Invalid order');
        }

        $trackingTable = DB::prefix() . 'instinctor_order_tracking';

        // Auto-generate tracking URL if carrier provided but no URL
        if ($trackingNumber && empty($trackingUrl) && $carrier !== 'other') {
            $trackingUrl = \Instinctor\Services\OrderTrackingService::generateTrackingUrl($carrier, $trackingNumber);
        }

        if ($trackingNumber) {
            // Upsert: check if tracking row exists for this order
            $existing = DB::get_row(DB::prepare(
                "SELECT id FROM $trackingTable WHERE order_id = %d ORDER BY created_at DESC LIMIT 1",
                $orderId
            ));

            if ($existing) {
                // Update existing tracking row
                DB::update($trackingTable, [
                    'carrier' => $carrier,
                    'tracking_number' => $trackingNumber,
                    'tracking_url' => $trackingUrl,
                    'last_update' => current_time('mysql'),
                ], ['id' => $existing->id]);
                // Sync update to AfterShip if enabled
                if (\Instinctor\Services\AfterShipService::isEnabled() && $trackingNumber) {
                    \Instinctor\Services\AfterShipService::onTrackingCreated($existing->id, [
                        'order_id'        => $orderId,
                        'tracking_number' => $trackingNumber,
                        'carrier'         => $carrier,
                    ]);
                }
            } else {
                // Insert new tracking row
                DB::insert($trackingTable, [
                    'order_id' => $orderId,
                    'carrier' => $carrier,
                    'tracking_number' => $trackingNumber,
                    'tracking_url' => $trackingUrl,
                    'status' => 'in_transit',
                    'shipped_at' => current_time('mysql'),
                ]);
                $newId = DB::insert_id();
                // Register new tracking with AfterShip if enabled
                if (\Instinctor\Services\AfterShipService::isEnabled() && $newId) {
                    \Instinctor\Services\AfterShipService::onTrackingCreated($newId, [
                        'order_id'        => $orderId,
                        'tracking_number' => $trackingNumber,
                        'carrier'         => $carrier,
                    ]);
                }
            }
        } else {
            // Tracking number cleared — remove tracking rows
            DB::delete($trackingTable, ['order_id' => $orderId]);
        }

        // Sync to orders.meta for quick admin reads and email templates
        $order = DB::get_row(DB::prepare(
            "SELECT meta FROM {$p}instinctor_orders WHERE id = %d",
            $orderId
        ));

        $meta = json_decode($order->meta ?? '{}', true) ?: [];
        $meta['tracking_number'] = $trackingNumber;
        $meta['tracking_url'] = $trackingUrl;
        $meta['tracking_carrier'] = $carrier;

        DB::update(
            DB::prefix() . 'instinctor_orders',
            ['meta' => json_encode($meta)],
            ['id' => $orderId]
        );

        // Add note
        if ($trackingNumber) {
            \Instinctor\Services\OrderNoteService::addSystemNote(
                $orderId,
                /* translators: %1\$s: tracking number, %2\$s: carrier name */
                sprintf(__('Tracking updated: %1\$s (%2\$s)', 'instinctor'), $trackingNumber, strtoupper($carrier))
            );
        }

        wp_send_json_success();
    }

    public function ajaxSendTrackingEmail(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        

        $p = DB::prefix();
        $orderId = intval($_POST['order_id'] ?? 0);

        $order = DB::get_row(DB::prepare(
            "SELECT * FROM {$p}instinctor_orders WHERE id = %d",
            $orderId
        ));

        if (!$order) {
            wp_send_json_error('Order not found');
        }

        // Get tracking info from tracking table (primary) with meta fallback
        $trackingRow = DB::get_row(DB::prepare(
            "SELECT carrier, tracking_number, tracking_url FROM {$p}instinctor_order_tracking WHERE order_id = %d ORDER BY created_at DESC LIMIT 1",
            $orderId
        ));
        
        if ($trackingRow && $trackingRow->tracking_number) {
            $trackingNumber = $trackingRow->tracking_number;
            $trackingUrl = $trackingRow->tracking_url ?? '';
        } else {
            $meta = json_decode($order->meta ?? '{}', true);
            $trackingNumber = $meta['tracking_number'] ?? '';
            $trackingUrl = $meta['tracking_url'] ?? '';
        }

        if (!$trackingNumber) {
            wp_send_json_error('No tracking number set');
        }

        $billing = json_decode($order->billing_address, true) ?: [];
        $customerName = trim(($billing['first_name'] ?? '') . ' ' . ($billing['last_name'] ?? ''));
        
        // Get customer email from billing address or customer record
        $customerEmail = $billing['email'] ?? '';
        if (empty($customerEmail) && $order->customer_id) {
            $customerEmail = DB::get_var(DB::prepare(
                "SELECT email FROM {$p}instinctor_customers WHERE id = %d",
                $order->customer_id
            )) ?: '';
        }
        
        if (empty($customerEmail)) {
            wp_send_json_error('No customer email found');
        }

        // Send email using EmailService
        $result = \Instinctor\Services\EmailService::send('order_shipped', $customerEmail, [
            'order_number' => $order->order_number,
            'customer_name' => $customerName,
            'customer_first_name' => $billing['first_name'] ?? '',
            'tracking_number' => $trackingNumber,
            'tracking_url' => $trackingUrl,
            'store_name' => get_bloginfo('name'),
            'store_url' => home_url(),
        ]);

        if ($result) {
            \Instinctor\Services\OrderNoteService::addSystemNote(
                $orderId,
                __('Shipping notification email sent to customer', 'instinctor')
            );
            wp_send_json_success();
        } else {
            wp_send_json_error('Failed to send email');
        }
    }

    public function ajaxProcessRefund(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }


        $p = DB::prefix();
        $orderId = intval($_POST['order_id'] ?? 0);
        $amount = floatval($_POST['amount'] ?? 0);
        $reason = sanitize_text_field($_POST['reason'] ?? '');
        $restoreStock = !empty($_POST['restore_stock']);
        $sendEmail = !isset($_POST['send_email']) || !empty($_POST['send_email']); // Default true

        if (!$orderId || $amount <= 0) {
            wp_send_json_error('Invalid order or amount');
        }

        $order = DB::get_row(DB::prepare(
            "SELECT o.*, c.email, c.first_name, c.last_name 
             FROM {$p}instinctor_orders o
             LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
             WHERE o.id = %d",
            $orderId
        ));

        if (!$order) {
            wp_send_json_error('Order not found');
        }

        // Check refundable amount
        $totalRefunded = \Instinctor\Services\RefundService::getOrderTotalRefunded($orderId);
        $refundable = $order->total - $totalRefunded;

                if ($amount > $refundable) {
            /* translators: %s: maximum refundable amount */
            wp_send_json_error(sprintf(__('Amount exceeds refundable amount ($%s)', 'instinctor'), number_format($refundable, 2)));
        }

        // Get line items if restoring stock
        $lineItems = [];
        if ($restoreStock) {
            $items = DB::get_results(DB::prepare(
                "SELECT product_id, quantity FROM {$p}instinctor_order_items WHERE order_id = %d",
                $orderId
            ));
            foreach ($items as $item) {
                $lineItems[] = [
                    'product_id' => $item->product_id,
                    'quantity' => $item->quantity,
                ];
            }
        }

        // Try Stripe refund if payment was via Stripe
        if ($order->payment_method === 'stripe' && !empty($order->payment_intent_id)) {
            $result = \Instinctor\Services\RefundService::processStripeRefund($orderId, $amount, $reason);
            if (!$result['success']) {
                wp_send_json_error($result['error']);
            }
        } else {
            // Manual refund (no payment gateway)
            \Instinctor\Services\RefundService::createRefund([
                'order_id' => $orderId,
                'amount' => $amount,
                'reason' => $reason,
                'restore_stock' => $restoreStock,
                'line_items' => $lineItems,
            ]);
        }
        
        // Add order note
        \Instinctor\Services\OrderNoteService::addSystemNote(
            $orderId,
            /* translators: %1\$s: refund amount, %2\$s: refund reason */
            sprintf(__('Refund of %1\$s processed. Reason: %2\$s', 'instinctor'), 
                \Instinctor\Services\CurrencyService::formatPrice($amount),
                $reason ?: 'Not specified'
            )
        );
        
        // Send refund notification email
        $customerEmail = $order->email ?: '';
        if (empty($customerEmail)) {
            $billing = json_decode($order->billing_address ?? '{}', true);
            $customerEmail = $billing['email'] ?? '';
        }
        if ($sendEmail && $customerEmail) {
            $customerName = trim(($order->first_name ?: '') . ' ' . ($order->last_name ?: ''));
            if (empty($customerName)) {
                $billing = $billing ?? json_decode($order->billing_address ?? '{}', true);
                $customerName = trim(($billing['first_name'] ?? '') . ' ' . ($billing['last_name'] ?? ''));
            }
            
            // Get order items for email
            $items = DB::get_results(DB::prepare(
                "SELECT * FROM {$p}instinctor_order_items WHERE order_id = %d",
                $orderId
            ));
            $orderDetails = $this->buildOrderDetailsHtml($items, $order);
            
            \Instinctor\Services\EmailService::send('order_refunded', $customerEmail, [
                'order_number' => $order->order_number,
                'order_date' => date_i18n(\Instinctor\Core\Config::get('date_format', 'Y-m-d'), strtotime($order->created_at)),
                'order_total' => \Instinctor\Services\CurrencyService::formatPrice($order->total),
                'order_details' => $orderDetails,
                'refund_amount' => \Instinctor\Services\CurrencyService::formatPrice($amount),
                'customer_name' => $customerName ?: 'Customer',
                'customer_first_name' => $order->first_name ?: ($billing['first_name'] ?? 'Customer'),
                'customer_email' => $customerEmail,
            ]);
        }

        wp_send_json_success(['message' => 'Refund processed' . ($sendEmail ? ' and email sent' : '')]);
    }
    
    /**
     * AJAX: Update order status and send appropriate email
     */
    public function ajaxUpdateOrderStatus(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        
        $orderId = intval($_POST['order_id'] ?? 0);
        $newStatus = sanitize_text_field($_POST['status'] ?? '');
        $sendEmail = !empty($_POST['send_email']);
        
        $validStatuses = ['pending', 'processing', 'shipped', 'completed', 'cancelled', 'refunded', 'on-hold'];
        
        if (!$orderId || !in_array($newStatus, $validStatuses)) {
            wp_send_json_error(['message' => 'Invalid order or status']);
        }

        $order = DB::get_row(DB::prepare(
            "SELECT o.*, c.email, c.first_name, c.last_name 
             FROM {$p}instinctor_orders o
             LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
             WHERE o.id = %d",
            $orderId
        ));

        if (!$order) {
            wp_send_json_error(['message' => 'Order not found']);
        }
        
        $oldStatus = $order->status;
        
        // Update status
        DB::update(
            DB::prefix() . 'instinctor_orders',
            ['status' => $newStatus, 'updated_at' => current_time('mysql')],
            ['id' => $orderId]
        );
        
        // Add order note
        \Instinctor\Services\OrderNoteService::addSystemNote(
            $orderId,
            /* translators: %1\$s: old status, %2\$s: new status */
            sprintf(__('Order status changed from %1\$s to %2\$s', 'instinctor'), $oldStatus, $newStatus)
        );
        
        // Fire action hook for other services to react
        do_action('instinctor_order_status_changed', $orderId, $oldStatus, $newStatus);
        
        if ($newStatus === 'completed') {
            do_action('instinctor_order_completed', $orderId);
        }
        if (in_array($newStatus, ['processing', 'completed']) && !in_array($oldStatus, ['processing', 'completed'])) {
            do_action('instinctor_order_paid', $orderId);
        }
        
        // Send email if requested — wrapped so email failure doesn't block status update
        $emailSent = false;
        if ($sendEmail && $order->email) {
            try {
                $this->sendOrderStatusEmail($order, $newStatus);
                $emailSent = true;
            } catch (\Throwable $e) {
                // Email failed but status was already updated
            }
        }
        
        wp_send_json_success(['message' => 'Status updated', 'status' => $newStatus, 'email_sent' => $emailSent]);
    }
    
    /**
     * Send order status change email
     */
    private function sendOrderStatusEmail(object $order, string $status): void
    {
        $p = DB::prefix();
        
        $items = DB::get_results(DB::prepare(
            "SELECT * FROM {$p}instinctor_order_items WHERE order_id = %d",
            $order->id
        ));
        
        $orderDetails = $this->buildOrderDetailsHtml($items, $order);
        $customerName = trim($order->first_name . ' ' . $order->last_name);
        
        $emailData = [
            'order_number' => $order->order_number,
            'order_date' => date_i18n(\Instinctor\Core\Config::get('date_format', 'Y-m-d'), strtotime($order->created_at)),
            'order_total' => \Instinctor\Services\CurrencyService::formatPrice($order->total),
            'order_details' => $orderDetails,
            'customer_name' => $customerName,
            'customer_first_name' => $order->first_name,
            'customer_email' => $order->email,
        ];
        
        // Map status to email template
        $templateMap = [
            'processing' => 'order_processing',
            'shipped' => 'order_shipped',
            'completed' => 'order_completed',
            'cancelled' => 'order_cancelled',
            'refunded' => 'order_refunded',
        ];
        
        if (isset($templateMap[$status])) {
            // Add tracking info for shipped status
            if ($status === 'shipped') {
                // Read from tracking table (primary) with meta fallback
                $trackingRow = DB::get_row(DB::prepare(
                    "SELECT tracking_number, tracking_url FROM {$p}instinctor_order_tracking WHERE order_id = %d ORDER BY created_at DESC LIMIT 1",
                    $order->id
                ));
                if ($trackingRow && $trackingRow->tracking_number) {
                    $trackingNumber = $trackingRow->tracking_number;
                    $trackingUrl = $trackingRow->tracking_url ?? '';
                } else {
                    $meta = json_decode($order->meta ?? '{}', true);
                    $trackingNumber = $meta['tracking_number'] ?? '';
                    $trackingUrl = $meta['tracking_url'] ?? '';
                }
                
                $trackingInfo = '';
                if ($trackingNumber) {
                    $trackingInfo = __('Tracking Number:', 'instinctor') . ' ' . $trackingNumber;
                    if ($trackingUrl) {
                        $trackingInfo .= "\n" . __('Track your package:', 'instinctor') . ' ' . $trackingUrl;
                    }
                }
                $emailData['tracking_number'] = $trackingNumber;
                $emailData['tracking_url'] = $trackingUrl;
                $emailData['tracking_info'] = $trackingInfo;
            }
            
            \Instinctor\Services\EmailService::send($templateMap[$status], $order->email, $emailData);
        }
    }
    
    /**
     * Build order details HTML for emails
     */
    private function buildOrderDetailsHtml(array $items, object $order): string
    {
        $html = '<table style="width:100%;border-collapse:collapse;margin:15px 0">';
        $html .= '<tr style="background:#f5f5f5"><th style="padding:10px;text-align:left;border-bottom:1px solid #ddd">Item</th><th style="padding:10px;text-align:center;border-bottom:1px solid #ddd">Qty</th><th style="padding:10px;text-align:right;border-bottom:1px solid #ddd">Price</th></tr>';
        
        foreach ($items as $item) {
            $html .= '<tr>';
            $html .= '<td style="padding:10px;border-bottom:1px solid #eee">' . esc_html($item->name);
            
            $itemMeta = json_decode($item->meta ?? '{}', true);
            if (!empty($itemMeta['variation_name'])) {
                $html .= '<br><small style="color:#666">' . esc_html($itemMeta['variation_name']) . '</small>';
            }
            
            $html .= '</td>';
            $html .= '<td style="padding:10px;text-align:center;border-bottom:1px solid #eee">' . intval($item->quantity) . '</td>';
            $html .= '<td style="padding:10px;text-align:right;border-bottom:1px solid #eee">' . \Instinctor\Services\CurrencyService::formatPrice($item->total) . '</td>';
            $html .= '</tr>';
        }
        
        $html .= '<tr><td colspan="2" style="padding:10px;text-align:right"><strong>Subtotal</strong></td><td style="padding:10px;text-align:right">' . \Instinctor\Services\CurrencyService::formatPrice($order->subtotal) . '</td></tr>';
        
        if ($order->shipping_total > 0) {
            $html .= '<tr><td colspan="2" style="padding:10px;text-align:right">Shipping</td><td style="padding:10px;text-align:right">' . \Instinctor\Services\CurrencyService::formatPrice($order->shipping_total) . '</td></tr>';
        }
        
        if ($order->tax_total > 0) {
            $html .= '<tr><td colspan="2" style="padding:10px;text-align:right">Tax</td><td style="padding:10px;text-align:right">' . \Instinctor\Services\CurrencyService::formatPrice($order->tax_total) . '</td></tr>';
        }
        
        if ($order->discount_total > 0) {
            $html .= '<tr><td colspan="2" style="padding:10px;text-align:right;color:#16a34a">Discount</td><td style="padding:10px;text-align:right;color:#16a34a">-' . \Instinctor\Services\CurrencyService::formatPrice($order->discount_total) . '</td></tr>';
        }
        
        $html .= '<tr style="background:#f5f5f5"><td colspan="2" style="padding:10px;text-align:right"><strong>Total</strong></td><td style="padding:10px;text-align:right"><strong>' . \Instinctor\Services\CurrencyService::formatPrice($order->total) . '</strong></td></tr>';
        $html .= '</table>';
        
        return $html;
    }

    public function ajaxSaveCurrency(): void
    {
        check_ajax_referer('instinctor_currency', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $data = [
            'code' => sanitize_text_field($_POST['code'] ?? ''),
            'name' => sanitize_text_field($_POST['name'] ?? ''),
            'symbol' => sanitize_text_field($_POST['symbol'] ?? ''),
            'exchange_rate' => floatval($_POST['exchange_rate'] ?? 1),
            'decimals' => intval($_POST['decimals'] ?? 2),
            'symbol_position' => sanitize_text_field($_POST['symbol_position'] ?? 'left'),
            'thousand_separator' => sanitize_text_field($_POST['thousand_separator'] ?? ','),
            'decimal_separator' => sanitize_text_field($_POST['decimal_separator'] ?? '.'),
            'enabled' => !empty($_POST['enabled']),
            'is_default' => !empty($_POST['is_default']),
            'auto_update' => !empty($_POST['auto_update']),
            'sort_order' => intval($_POST['sort_order'] ?? 0),
        ];

        if (empty($data['code']) || strlen($data['code']) !== 3) {
            wp_send_json_error(['message' => 'Invalid currency code']);
        }
        
        // Block Russian Ruble
        if (strtoupper($data['code']) === 'RUB') {
            wp_send_json_error(['message' => 'This currency is not supported']);
        }

        $id = \Instinctor\Services\CurrencyService::saveCurrency($data);
        
        wp_send_json_success(['id' => $id]);
    }

    public function ajaxDeleteCurrency(): void
    {
        check_ajax_referer('instinctor_currency', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id = intval($_POST['id'] ?? 0);
        
        if (!$id) {
            wp_send_json_error(['message' => 'Invalid currency ID']);
        }

        $success = \Instinctor\Services\CurrencyService::deleteCurrency($id);
        
        if (!$success) {
            wp_send_json_error(['message' => 'Cannot delete the default currency']);
        }

        wp_send_json_success();
    }

    public function ajaxUpdateExchangeRates(): void
    {
        check_ajax_referer('instinctor_currency', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $success = \Instinctor\Services\CurrencyService::updateExchangeRates();
        
        if (!$success) {
            wp_send_json_error(['message' => 'Failed to update rates. Check your API key.']);
        }

        wp_send_json_success(['message' => 'Exchange rates updated successfully']);
    }

    public function ajaxSaveOption(): void
    {
        check_ajax_referer('instinctor_currency', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $option = sanitize_text_field($_POST['option'] ?? '');
        $value = sanitize_text_field($_POST['value'] ?? '');
        
        // Only allow specific options
        $allowedOptions = [
            'instinctor_exchange_rate_api',
            'instinctor_auto_detect_currency',
        ];
        
        if (!in_array($option, $allowedOptions)) {
            wp_send_json_error(['message' => 'Invalid option']);
        }

        update_option($option, $value);
        
        wp_send_json_success();
    }

    /**
     * AJAX: Toggle email template enabled status
     */
    /**
     * Intercept WordPress password reset email when Instinctor template is enabled.
     * Sends via relay and suppresses WP's own email to avoid duplicates.
     */
    public function handlePasswordResetEmail(string $message, string $key, string $userLogin, \WP_User $userData): string
    {
        $template = \Instinctor\Services\EmailService::getTemplate('customer_reset_password');
        if (empty($template['enabled'])) {
            return $message; // Let WordPress handle it normally
        }

        $resetLink = network_site_url(
            'wp-login.php?action=rp&key=' . $key . '&login=' . rawurlencode($userLogin),
            'login'
        );

        \Instinctor\Services\EmailService::send('customer_reset_password', $userData->user_email, [
            'customer_name'  => $userData->display_name,
            'customer_email' => $userData->user_email,
            'reset_link'     => $resetLink,
        ]);

        // Suppress WordPress's own email by clearing recipients in the next wp_mail call
        add_action('phpmailer_init', function (\PHPMailer\PHPMailer\PHPMailer $phpmailer): void {
            static $suppressed = false;
            if (!$suppressed) {
                $phpmailer->clearAllRecipients();
                $suppressed = true;
            }
        });

        return $message;
    }

    public function ajaxToggleEmailTemplate(): void
    {
        check_ajax_referer('instinctor_toggle_email', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $type = sanitize_text_field($_POST['type'] ?? '');
        $enabled = !empty($_POST['enabled']);
        
        if (empty($type)) {
            wp_send_json_error('Invalid template type');
        }
        
        $templates = Config::get('instinctor_email_templates', []);
        if (!isset($templates[$type])) {
            $templates[$type] = [];
        }
        $templates[$type]['enabled'] = $enabled;
        Config::set('instinctor_email_templates', $templates);
        
        wp_send_json_success();
    }

    /**
     * AJAX: Save email footer settings
     */
    public function ajaxSaveFooterSettings(): void
    {
        check_ajax_referer('instinctor_save_footer', 'instinctor_footer_nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $settings = [
            'show_store_name' => isset($_POST['footer_store_name']),
            'show_address' => isset($_POST['footer_address']),
            'show_city_state_zip' => isset($_POST['footer_city_state_zip']),
            'show_country' => isset($_POST['footer_country']),
            'show_email' => isset($_POST['footer_email']),
            'show_phone' => isset($_POST['footer_phone']),
            'show_social' => isset($_POST['footer_social']),
            'custom_text' => sanitize_text_field($_POST['footer_custom_text'] ?? ''),
            // Styling options
            'text_align' => sanitize_text_field($_POST['footer_text_align'] ?? 'center'),
            'show_logo' => isset($_POST['footer_show_logo']),
            'store_name_size' => sanitize_text_field($_POST['footer_store_name_size'] ?? '14'),
            'store_name_weight' => sanitize_text_field($_POST['footer_store_name_weight'] ?? '600'),
            'address_size' => sanitize_text_field($_POST['footer_address_size'] ?? '12'),
            'address_weight' => sanitize_text_field($_POST['footer_address_weight'] ?? '400'),
            'contact_size' => sanitize_text_field($_POST['footer_contact_size'] ?? '12'),
            'contact_weight' => sanitize_text_field($_POST['footer_contact_weight'] ?? '400'),
            'custom_size' => sanitize_text_field($_POST['footer_custom_size'] ?? '11'),
            'custom_weight' => sanitize_text_field($_POST['footer_custom_weight'] ?? '400'),
        ];
        
        \Instinctor\Services\EmailService::saveFooterSettings($settings);
        
        wp_send_json_success($settings);
    }

    /**
     * AJAX: Newsletter subscription (public)
     */
    public function ajaxNewsletterSubscribe(): void
    {
        // Public-facing form — verify the frontend nonce minted via wp_create_nonce('instinctor_frontend')
        // and exposed in ImpulseConfig.nonce. Prevents drive-by signups from foreign origins.
        check_ajax_referer('instinctor_frontend', 'nonce');
        $email = sanitize_email(wp_unslash($_POST['email'] ?? ''));
        
        if (!is_email($email)) {
            wp_send_json_error(['message' => 'Invalid email address']);
        }
        
        // Get existing subscribers
        $subscribers = Config::get('instinctor_newsletter_subscribers', []);
        
        // Check if already subscribed
        if (in_array($email, $subscribers)) {
            wp_send_json_success(['message' => 'Already subscribed']);
        }
        
        // Add subscriber
        $subscribers[] = $email;
        Config::set('instinctor_newsletter_subscribers', $subscribers);
        
        // Hook for integrations (Mailchimp, etc.)
        do_action('instinctor_newsletter_subscribed', $email);
        
        wp_send_json_success(['message' => 'Successfully subscribed']);
    }

    // v1.3.0 AJAX Handlers

    public function ajaxAddTracking(): void
    {
        check_ajax_referer('instinctor_tracking', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id = \Instinctor\Services\OrderTrackingService::addTracking([
            'order_id' => intval($_POST['order_id'] ?? 0),
            'carrier' => sanitize_text_field($_POST['carrier'] ?? ''),
            'tracking_number' => sanitize_text_field($_POST['tracking_number'] ?? ''),
            'tracking_url' => esc_url_raw($_POST['tracking_url'] ?? ''),
            'estimated_delivery' => sanitize_text_field($_POST['estimated_delivery'] ?? ''),
            'notify_customer' => !empty($_POST['notify_customer']),
        ]);

        if (!$id) {
            wp_send_json_error(['message' => 'Failed to add tracking']);
        }

        wp_send_json_success(['id' => $id]);
    }

    public function ajaxUpdateTrackingStatus(): void
    {
        check_ajax_referer('instinctor_tracking', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id = intval($_POST['id'] ?? 0);
        $status = sanitize_text_field($_POST['status'] ?? '');

        if (\Instinctor\Services\OrderTrackingService::updateStatus($id, $status)) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => 'Failed to update status']);
        }
    }

    public function ajaxDeleteTracking(): void
    {
        check_ajax_referer('instinctor_tracking', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id = intval($_POST['id'] ?? 0);

        if (\Instinctor\Services\OrderTrackingService::delete($id)) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => 'Failed to delete tracking']);
        }
    }

    public function ajaxUpdateStock(): void
    {
        check_ajax_referer('instinctor_inventory', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $productId = intval($_POST['product_id'] ?? 0);
        $quantity = intval($_POST['quantity'] ?? 0);

        if (\Instinctor\Services\InventoryService::setStock($productId, $quantity)) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => 'Failed to update stock']);
        }
    }

    public function ajaxExportInventory(): void
    {
        check_ajax_referer('instinctor_inventory', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_die('Permission denied');
        }

        $products = DB::get_results(
            "SELECT id, name, sku, price, stock_quantity, stock_status 
             FROM {$p}instinctor_products 
             WHERE status = 'published' 
             ORDER BY name"
        );

        header('Content-Type: text/csv');
        header('Content-Disposition: attachment; filename="inventory-' . date('Y-m-d') . '.csv"');

        $output = fopen('php://output', 'w');
        fputcsv($output, ['ID', 'Name', 'SKU', 'Price', 'Stock Quantity', 'Stock Status']);

        foreach ($products as $p) {
            fputcsv($output, [
                $p->id,
                $p->name,
                $p->sku,
                $p->price,
                $p->stock_quantity,
                $p->stock_status,
            ]);
        }

        fclose($output);
        exit;
    }

    public function ajaxUpdateRMAStatus(): void
    {
        check_ajax_referer('instinctor_rma', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $id     = intval($_POST['id'] ?? 0);
        $status = sanitize_text_field($_POST['status'] ?? '');
        if (\Instinctor\Services\RMAService::updateStatus($id, $status, Auth::userId())) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => 'Failed to update status']);
        }
    }

    public function ajaxGetRMAs(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        $p = DB::prefix();
        $args = [
            'status' => sanitize_text_field($_POST['status'] ?? ''),
            'search' => sanitize_text_field($_POST['search'] ?? ''),
            'limit'  => 50,
        ];
        $rmas  = \Instinctor\Services\RMAService::getAll($args);

        // Enrich with order_number and customer name
        foreach ($rmas as &$rma) {
            if ($rma->order_id) {
                $orderRow = DB::get_row(DB::prepare(
                    "SELECT o.order_number, c.first_name, c.last_name, c.email
                     FROM {$p}instinctor_orders o
                     LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
                     WHERE o.id = %d",
                    $rma->order_id
                ));
                $rma->order_number = $orderRow->order_number ?? ('#' . $rma->order_id);
                $rma->customer_name = $orderRow ? trim(($orderRow->first_name ?: '') . ' ' . ($orderRow->last_name ?: '')) : '';
                $rma->customer_email = $orderRow->email ?? '';
            }
        }
        unset($rma);

        // Stats from unfiltered data
        $allStats = DB::get_results("SELECT status, COUNT(*) as cnt FROM {$p}instinctor_rma GROUP BY status");
        $stats = ['pending' => 0, 'approved' => 0, 'shipped' => 0, 'received' => 0, 'refunded' => 0, 'completed' => 0, 'rejected' => 0, 'total' => 0];
        foreach ($allStats as $row) {
            $stats[$row->status] = (int)$row->cnt;
            $stats['total'] += (int)$row->cnt;
        }

        wp_send_json_success(['rmas' => $rmas, 'stats' => $stats]);
    }

    public function ajaxGetRMA(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        $p = DB::prefix();
        $id  = intval($_POST['id'] ?? 0);
        $rma = \Instinctor\Services\RMAService::getById($id);
        if (!$rma) wp_send_json_error('Not found');

        // Enrich with order_number and customer
        if ($rma->order_id) {
            $orderRow = DB::get_row(DB::prepare(
                "SELECT o.order_number, c.first_name, c.last_name, c.email, c.id as customer_id
                 FROM {$p}instinctor_orders o
                 LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
                 WHERE o.id = %d",
                $rma->order_id
            ));
            $rma->order_number = $orderRow->order_number ?? ('#' . $rma->order_id);
            $rma->customer_name = $orderRow ? trim(($orderRow->first_name ?: '') . ' ' . ($orderRow->last_name ?: '')) : '';
            $rma->customer_email = $orderRow->email ?? '';
            $rma->customer_id = $orderRow->customer_id ?? null;
        }

        wp_send_json_success(['rma' => $rma, 'currency' => Config::get('instinctor_currency_symbol', '$')]);
    }

    public function ajaxApproveRMA(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        $id            = intval($_POST['id'] ?? 0);
        $refundAmount  = floatval($_POST['refund_amount'] ?? 0);
        $restockingFee = floatval($_POST['restocking_fee'] ?? 0);
        $notes         = sanitize_textarea_field($_POST['notes'] ?? '');
        if (\Instinctor\Services\RMAService::approve($id, $refundAmount, $restockingFee, $notes ?: null)) {
            wp_send_json_success(['rma' => \Instinctor\Services\RMAService::getById($id), 'currency' => Config::get('instinctor_currency_symbol', '$')]);
        } else {
            wp_send_json_error('Failed to approve');
        }
    }

    public function ajaxRejectRMA(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        $id     = intval($_POST['id'] ?? 0);
        $reason = sanitize_textarea_field($_POST['reason'] ?? '');
        if (\Instinctor\Services\RMAService::reject($id, $reason)) {
            wp_send_json_success();
        } else {
            wp_send_json_error('Failed to reject');
        }
    }

    /**
     * AJAX: Immediate refund from approved RMA — skips shipped/received steps.
     * Hits Stripe API if order was paid via Stripe, then marks RMA as refunded.
     */
    public function ajaxRMARefundNow(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');

        $p  = DB::prefix();
        $id = intval($_POST['id'] ?? 0);
        $rma = \Instinctor\Services\RMAService::getById($id);

        if (!$rma || $rma->status !== 'approved') {
            wp_send_json_error('RMA must be in approved status to refund');
        }

        if (!$rma->refund_amount || $rma->refund_amount <= 0) {
            wp_send_json_error('No refund amount set');
        }

        // Get order to check payment method
        $order = DB::get_row(DB::prepare(
            "SELECT payment_method, payment_intent_id, order_number FROM {$p}instinctor_orders WHERE id = %d",
            $rma->order_id
        ));

        $refundType = $rma->refund_type ?? 'original';

        if ($refundType === 'store_credit') {
            // Store credit path — processRefund handles gift card creation + status update
            if (!\Instinctor\Services\RMAService::processRefund($id)) {
                wp_send_json_error('Failed to process store credit');
            }
        } elseif ($order && $order->payment_method === 'stripe' && !empty($order->payment_intent_id)) {
            // Stripe path — processStripeRefund already creates the DB refund record
            $result = \Instinctor\Services\RefundService::processStripeRefund(
                $rma->order_id,
                $rma->refund_amount,
                'RMA: ' . ($rma->rma_number ?? $id)
            );
            if (!$result['success']) {
                wp_send_json_error($result['error'] ?? 'Stripe refund failed');
            }
            // Just update RMA status — DB record already created by processStripeRefund
            \Instinctor\Services\RMAService::updateStatus($id, 'refunded', \Instinctor\Core\Auth::userId());
        } else {
            // Manual/non-Stripe path — processRefund creates the DB record + status update
            if (!\Instinctor\Services\RMAService::processRefund($id)) {
                wp_send_json_error('Failed to process refund');
            }
        }

        // Add order note
        \Instinctor\Services\OrderNoteService::addSystemNote(
            $rma->order_id,
            sprintf('Immediate refund of %s processed via RMA %s',
                \Instinctor\Services\CurrencyService::formatPrice($rma->refund_amount),
                $rma->rma_number ?? $id
            )
        );

        // Return updated RMA
        $updated = \Instinctor\Services\RMAService::getById($id);
        if ($updated && $updated->order_id) {
            $orderRow = DB::get_row(DB::prepare(
                "SELECT o.order_number, c.first_name, c.last_name, c.email, c.id as customer_id
                 FROM {$p}instinctor_orders o LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
                 WHERE o.id = %d", $updated->order_id
            ));
            $updated->order_number = $orderRow->order_number ?? '';
            $updated->customer_name = $orderRow ? trim(($orderRow->first_name ?: '') . ' ' . ($orderRow->last_name ?: '')) : '';
            $updated->customer_email = $orderRow->email ?? '';
            $updated->customer_id = $orderRow->customer_id ?? null;
        }

        wp_send_json_success(['rma' => $updated, 'currency' => Config::get('instinctor_currency_symbol', '$')]);
    }

    public function ajaxGetRMASettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        wp_send_json_success(['settings' => [
            'return_days'       => intval(Config::get('instinctor_return_days', 30)),
            'restocking_fee'    => floatval(Config::get('instinctor_restocking_fee', 0)),
            'notify_admin'      => (bool) Config::get('instinctor_rma_notify_admin', true),
            'auto_approve'      => (bool) Config::get('instinctor_rma_auto_approve', false),
        ]]);
    }

    public function ajaxSaveRMASettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        Config::set('instinctor_return_days',       max(1, intval($_POST['return_days'] ?? 30)));
        Config::set('instinctor_restocking_fee',    max(0, floatval($_POST['restocking_fee'] ?? 0)));
        Config::set('instinctor_rma_notify_admin',  !empty($_POST['notify_admin']));
        Config::set('instinctor_rma_auto_approve',  !empty($_POST['auto_approve']));
        wp_send_json_success();
    }

    public function ajaxGetTrackingSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        $aftership = \Instinctor\Services\AfterShipService::getSettings();
        wp_send_json_success(['settings' => [
            'aftership_enabled'    => $aftership['enabled'] ?? false,
            'aftership_api_key'    => $aftership['api_key'] ?? '',
            'aftership_frequency'  => $aftership['update_frequency'] ?? 'hourly',
            'notify_on_ship'       => (bool) Config::get('instinctor_tracking_notify_ship', true),
            'notify_on_delivery'   => (bool) Config::get('instinctor_tracking_notify_delivery', true),
            'tracking_page'        => Config::get('instinctor_tracking_page_url', ''),
        ]]);
    }

    public function ajaxSaveAfterShipSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) wp_send_json_error('Permission denied');
        \Instinctor\Services\AfterShipService::saveSettings([
            'enabled'          => !empty($_POST['aftership_enabled']),
            'api_key'          => sanitize_text_field($_POST['aftership_api_key'] ?? ''),
            'update_frequency' => sanitize_text_field($_POST['aftership_frequency'] ?? 'hourly'),
        ]);
        Config::set('instinctor_tracking_notify_ship',     !empty($_POST['notify_on_ship']));
        Config::set('instinctor_tracking_notify_delivery', !empty($_POST['notify_on_delivery']));
        Config::set('instinctor_tracking_page_url',        esc_url_raw($_POST['tracking_page'] ?? ''));
        wp_send_json_success();
    }



    public function ajaxPrintInvoice(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_die('Permission denied');
        }

        $orderId = intval($_GET['order_id'] ?? 0);
        
        if (!$orderId) {
            wp_die('Invalid order');
        }

        $html = \Instinctor\Services\InvoiceService::generateInvoice($orderId);

        if (!$html) {
            wp_die('Order not found');
        }

        // Invoice is a complete HTML document (<!doctype>/<head>/<style>/<body>)
        // generated by InvoiceService, which escapes every dynamic value at the point
        // of output (esc_html/esc_url/esc_attr). wp_kses_post would strip the
        // doctype/head/style and break the printable invoice, so it is emitted as-is.
        echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        exit;
    }

    public function ajaxPrintPackingSlip(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_die('Permission denied');
        }

        $orderId = intval($_GET['order_id'] ?? 0);
        
        if (!$orderId) {
            wp_die('Invalid order');
        }

        $html = \Instinctor\Services\InvoiceService::generatePackingSlip($orderId);

        if (!$html) {
            wp_die('Order not found');
        }

        // Packing slip is a complete HTML document (<!doctype>/<head>/<style>/<body>)
        // generated by InvoiceService, which escapes every dynamic value at the point
        // of output (esc_html/esc_url/esc_attr). wp_kses_post would strip the
        // doctype/head/style and break the layout, so it is emitted as-is.
        echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        exit;
    }
    
    /**
     * Get products for pricing table dropdown
     */
    public function ajaxGetProducts(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $p = DB::prefix();
        
        $products = DB::get_results("
            SELECT p.id, p.name, p.price 
            FROM {$p}instinctor_products p 
            WHERE p.status = 'published' 
            ORDER BY p.name ASC 
            LIMIT 100
        ");
        
        $result = [];
        foreach ($products as $product) {
            $result[] = [
                'id' => $product->id,
                'title' => $product->name,
                'price' => '$' . number_format(floatval($product->price), 2)
            ];
        }
        
        wp_send_json_success($result);
    }
    
    /**
     * Get subscription plans for pricing table dropdown
     */
    public function ajaxGetSubscriptionPlans(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $p = DB::prefix();
        
        // Check if subscription plans table exists
        $table = DB::prefix() . 'instinctor_subscription_plans';
        $tableExists = DB::get_var("SHOW TABLES LIKE '$table'") === $table;
        
        if (!$tableExists) {
            // Fall back to products with subscription type
            $products = DB::get_results("
                SELECT p.id, p.name 
                FROM {$p}instinctor_products p 
                WHERE p.product_type = 'subscription' AND p.status = 'published' 
                ORDER BY p.name ASC 
                LIMIT 50
            ");
            
            $result = [];
            foreach ($products as $product) {
                $result[] = [
                    'id' => $product->id,
                    'name' => $product->name
                ];
            }
            
            wp_send_json_success($result);
            return;
        }
        
        $plans = DB::get_results("
            SELECT id, name 
            FROM {$table} 
            WHERE status = 'active' 
            ORDER BY name ASC 
            LIMIT 50
        ");
        
        $result = [];
        foreach ($plans as $plan) {
            $result[] = [
                'id' => $plan->id,
                'name' => $plan->name
            ];
        }
        
        wp_send_json_success($result);
    }



    /**
     * AJAX: Save card design
     */
    public function ajaxSaveCardDesign(): void
    {
        check_ajax_referer('instinctor_card_designer', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $design = isset($_POST['design']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['design'])), true) : null;
        
        if (!$design) {
            wp_send_json_error('Invalid design data');
        }
        
        // Sanitize the design data
        $sanitized = $this->sanitizeCardDesign($design);
        
        Config::set('instinctor_card_design', $sanitized);
        wp_send_json_success(['message' => 'Design saved']);
    }
    
    /**
     * Sanitize card design data
     */
    private function sanitizeCardDesign(array $design): array
    {
        $sanitized = [];
        
        // Preset
        $sanitized['preset'] = sanitize_text_field($design['preset'] ?? 'modern');
        
        // Elements (array of strings)
        $valid_elements = ['image', 'title', 'price', 'add_to_cart', 'quick_view', 'sale_badge', 'description', 'sku', 'stock'];
        $sanitized['elements'] = array_filter(
            array_map('sanitize_text_field', $design['elements'] ?? []),
            function($el) use ($valid_elements) { return in_array($el, $valid_elements); }
        );
        
        // Card settings
        $sanitized['card'] = [
            'background' => sanitize_hex_color($design['card']['background'] ?? '#ffffff'),
            'border_color' => sanitize_hex_color($design['card']['border_color'] ?? '#e0e0e0'),
            'border_width' => max(0, min(5, intval($design['card']['border_width'] ?? 1))),
            'border_radius' => max(0, min(50, intval($design['card']['border_radius'] ?? 8))),
            'shadow' => in_array($design['card']['shadow'] ?? 'small', ['none', 'small', 'medium', 'large']) ? $design['card']['shadow'] : 'small',
            'padding' => max(0, min(30, intval($design['card']['padding'] ?? 0)))
        ];
        
        // Image settings
        $sanitized['image'] = [
            'aspect_ratio' => in_array($design['image']['aspect_ratio'] ?? 'square', ['square', 'portrait', 'landscape']) ? $design['image']['aspect_ratio'] : 'square',
            'border_radius' => max(0, min(50, intval($design['image']['border_radius'] ?? 8)))
        ];
        
        // Title settings
        $sanitized['title'] = [
            'font_size' => max(10, min(32, intval($design['title']['font_size'] ?? 16))),
            'font_weight' => in_array($design['title']['font_weight'] ?? '600', ['300', '400', '500', '600', '700', '800']) ? $design['title']['font_weight'] : '600',
            'color' => sanitize_hex_color($design['title']['color'] ?? '#1d2327'),
            'margin_top' => max(0, min(30, intval($design['title']['margin_top'] ?? 12))),
            'lines' => max(1, min(5, intval($design['title']['lines'] ?? 2)))
        ];
        
        // Price settings
        $sanitized['price'] = [
            'font_size' => max(10, min(32, intval($design['price']['font_size'] ?? 18))),
            'font_weight' => in_array($design['price']['font_weight'] ?? '700', ['400', '500', '600', '700', '800']) ? $design['price']['font_weight'] : '700',
            'color' => sanitize_hex_color($design['price']['color'] ?? '#1d2327'),
            'sale_color' => sanitize_hex_color($design['price']['sale_color'] ?? '#dc2626'),
            'margin_top' => max(0, min(30, intval($design['price']['margin_top'] ?? 8)))
        ];
        
        // Add to cart settings
        $sanitized['add_to_cart'] = [
            'style' => in_array($design['add_to_cart']['style'] ?? 'button', ['button', 'link', 'icon']) ? $design['add_to_cart']['style'] : 'button',
            'bg_color' => sanitize_hex_color($design['add_to_cart']['bg_color'] ?? '#1d2327'),
            'text_color' => sanitize_hex_color($design['add_to_cart']['text_color'] ?? '#ffffff'),
            'border_radius' => max(0, min(50, intval($design['add_to_cart']['border_radius'] ?? 6))),
            'full_width' => !empty($design['add_to_cart']['full_width']),
            'margin_top' => max(0, min(30, intval($design['add_to_cart']['margin_top'] ?? 12))),
            'button_text' => sanitize_text_field($design['add_to_cart']['button_text'] ?? 'Add to Cart')
        ];
        
        // Sale badge settings
        $sanitized['sale_badge'] = [
            'enabled' => $design['sale_badge']['enabled'] ?? true,
            'bg_color' => sanitize_hex_color($design['sale_badge']['bg_color'] ?? '#dc2626'),
            'text_color' => sanitize_hex_color($design['sale_badge']['text_color'] ?? '#ffffff'),
            'text' => sanitize_text_field($design['sale_badge']['text'] ?? 'Sale')
        ];
        
        // Quick view settings
        $sanitized['quick_view'] = [
            'enabled' => !empty($design['quick_view']['enabled']),
            'style' => in_array($design['quick_view']['style'] ?? 'icon', ['icon', 'button']) ? $design['quick_view']['style'] : 'icon'
        ];
        
        return $sanitized;
    }

    /**
     * AJAX: Save card design as template
     */
    public function ajaxSaveCardTemplate(): void
    {
        check_ajax_referer('instinctor_card_designer', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
        $design = isset($_POST['design']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['design'])), true) : null;
        
        if (empty($name) || !$design) {
            wp_send_json_error('Missing template name or design data');
        }
        
        // Sanitize the design
        $sanitized = $this->sanitizeCardDesign($design);
        
        // Generate slug from name
        $slug = sanitize_title($name);
        
        // Get existing templates
        $templates = Config::get('instinctor_card_templates', []);
        
        // Add/update template
        $templates[$slug] = [
            'name' => $name,
            'design' => $sanitized,
            'created' => isset($templates[$slug]) ? $templates[$slug]['created'] : current_time('mysql'),
            'updated' => current_time('mysql')
        ];
        
        Config::set('instinctor_card_templates', $templates);
        
        wp_send_json_success([
            'message' => 'Template saved',
            'slug' => $slug,
            'templates' => $this->getCardTemplatesList()
        ]);
    }
    
    /**
     * AJAX: Delete card template
     */
    public function ajaxDeleteCardTemplate(): void
    {
        check_ajax_referer('instinctor_card_designer', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
        
        if (empty($slug)) {
            wp_send_json_error('Missing template slug');
        }
        
        $templates = Config::get('instinctor_card_templates', []);
        
        if (isset($templates[$slug])) {
            unset($templates[$slug]);
            Config::set('instinctor_card_templates', $templates);
        }
        
        wp_send_json_success([
            'message' => 'Template deleted',
            'templates' => $this->getCardTemplatesList()
        ]);
    }
    
    /**
     * AJAX: Load card template
     */
    public function ajaxLoadCardTemplate(): void
    {
        check_ajax_referer('instinctor_card_designer', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
        
        if (empty($slug)) {
            wp_send_json_error('Missing template slug');
        }
        
        $templates = Config::get('instinctor_card_templates', []);
        
        if (!isset($templates[$slug])) {
            wp_send_json_error('Template not found');
        }
        
        wp_send_json_success([
            'design' => $templates[$slug]['design']
        ]);
    }
    
    /**
     * Get card templates list for dropdown
     */
    public function getCardTemplatesList(): array
    {
        $templates = Config::get('instinctor_card_templates', []);
        $list = [];
        
        foreach ($templates as $slug => $template) {
            $list[] = [
                'slug' => $slug,
                'name' => $template['name']
            ];
        }
        
        return $list;
    }
    
    /**
     * AJAX: Save header as template
     */
    public function ajaxSaveHeaderTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
        $settings = isset($_POST['settings']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['settings'])), true) : [];
        
        if (empty($name)) {
            wp_send_json_error('Missing template name');
        }
        
        $slug = sanitize_title($name) . '_' . time();
        $templates = Config::get('instinctor_header_templates', []);
        
        $templates[$slug] = [
            'name' => $name,
            'settings' => $settings,
            'created' => current_time('mysql')
        ];
        
        Config::set('instinctor_header_templates', $templates);
        
        wp_send_json_success([
            'message' => 'Template saved',
            'slug' => $slug,
            'templates' => $this->getHeaderTemplatesList()
        ]);
    }
    
    /**
     * AJAX: Save header globally
     */
    public function ajaxSaveHeaderGlobal(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $settings = isset($_POST['settings']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['settings'])), true) : [];
        
        Config::set('instinctor_global_header', $settings);
        
        wp_send_json_success(['message' => 'Header saved globally']);
    }
    
    /**
     * AJAX: Load header template
     */
    public function ajaxLoadHeaderTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
        
        if (empty($slug)) {
            wp_send_json_error('Missing template slug');
        }
        
        $templates = Config::get('instinctor_header_templates', []);
        
        if (!isset($templates[$slug])) {
            wp_send_json_error('Template not found');
        }
        
        wp_send_json_success([
            'settings' => $templates[$slug]['settings']
        ]);
    }
    
    /**
     * Get header templates list
     */
    public function getHeaderTemplatesList(): array
    {
        $templates = Config::get('instinctor_header_templates', []);
        $list = [];
        
        foreach ($templates as $slug => $template) {
            $list[] = ['slug' => $slug, 'name' => $template['name']];
        }
        
        return $list;
    }
    
    /**
     * AJAX: Save footer as template
     */
    public function ajaxSaveFooterTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
        $settings = isset($_POST['settings']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['settings'])), true) : [];
        
        if (empty($name)) {
            wp_send_json_error('Missing template name');
        }
        
        $slug = sanitize_title($name) . '_' . time();
        $templates = Config::get('instinctor_footer_templates', []);
        
        $templates[$slug] = [
            'name' => $name,
            'settings' => $settings,
            'created' => current_time('mysql')
        ];
        
        Config::set('instinctor_footer_templates', $templates);
        
        wp_send_json_success([
            'message' => 'Template saved',
            'slug' => $slug,
            'templates' => $this->getFooterTemplatesList()
        ]);
    }
    
    /**
     * AJAX: Save footer globally
     */
    public function ajaxSaveFooterGlobal(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $settings = isset($_POST['settings']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['settings'])), true) : [];
        
        Config::set('instinctor_global_footer', $settings);
        
        wp_send_json_success(['message' => 'Footer saved globally']);
    }
    
    /**
     * AJAX: Load footer template
     */
    public function ajaxLoadFooterTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
        
        if (empty($slug)) {
            wp_send_json_error('Missing template slug');
        }
        
        $templates = Config::get('instinctor_footer_templates', []);
        
        if (!isset($templates[$slug])) {
            wp_send_json_error('Template not found');
        }
        
        wp_send_json_success([
            'settings' => $templates[$slug]['settings']
        ]);
    }
    
    /**
     * Get footer templates list
     */
    public function getFooterTemplatesList(): array
    {
        $templates = Config::get('instinctor_footer_templates', []);
        $list = [];
        
        foreach ($templates as $slug => $template) {
            $list[] = ['slug' => $slug, 'name' => $template['name']];
        }
        
        return $list;
    }
    
    /**
     * AJAX: Get header templates
     */
    public function ajaxGetHeaderTemplates(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        wp_send_json_success([
            'templates' => $this->getHeaderTemplatesList()
        ]);
    }
    
    /**
     * AJAX: Delete header template
     */
    public function ajaxDeleteHeaderTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
        
        if (empty($slug)) {
            wp_send_json_error('Missing template slug');
        }
        
        $templates = Config::get('instinctor_header_templates', []);
        
        if (isset($templates[$slug])) {
            unset($templates[$slug]);
            Config::set('instinctor_header_templates', $templates);
        }
        
        wp_send_json_success(['message' => 'Template deleted']);
    }
    
    /**
     * AJAX: Get footer templates
     */
    public function ajaxGetFooterTemplates(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        wp_send_json_success([
            'templates' => $this->getFooterTemplatesList()
        ]);
    }
    
    /**
     * AJAX: Delete footer template
     */
    public function ajaxDeleteFooterTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
        
        if (empty($slug)) {
            wp_send_json_error('Missing template slug');
        }
        
        $templates = Config::get('instinctor_footer_templates', []);
        
        if (isset($templates[$slug])) {
            unset($templates[$slug]);
            Config::set('instinctor_footer_templates', $templates);
        }
        
        wp_send_json_success(['message' => 'Template deleted']);
    }

    /**
     * AJAX: Save global header settings
     */
    public function ajaxSaveGlobalHeader(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- raw JSON; wp_unslash + json_decode below; structure validated post-decode
        $settings = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '';

        if (empty($settings)) {
            wp_send_json_error('Missing settings');
        }

        $decoded = json_decode((string) $settings, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            wp_send_json_error('Invalid JSON');
        }

        // Sanitize every leaf string in the decoded settings tree before
        // storing. Reviewer flagged the prior path (decode → set) as
        // "no field sanitization". sanitize_text_field strips control
        // chars, tags, slashes; safe for the field shapes this endpoint
        // accepts (plain text labels, URLs, color codes). The function is
        // defined inline once at file scope further down.
        $decoded = self::sanitizeSettingsTreeRecursive($decoded);

        Config::set('instinctor_global_header', $decoded);

        wp_send_json_success(['message' => 'Global header saved']);
    }

    /**
     * Recursively sanitize a settings tree's leaf strings. Used by the
     * global-header / global-footer save endpoints (and any future endpoint
     * accepting a JSON settings blob from an authenticated admin).
     * Required by WP.org review — every $_POST-derived value must pass
     * through a WordPress sanitization function before persistence.
     */
    private static function sanitizeSettingsTreeRecursive($value)
    {
        if (is_array($value)) {
            $out = [];
            foreach ($value as $k => $v) {
                // Keys: limit to printable ASCII identifier chars.
                $clean_k = is_string($k) ? sanitize_key((string) $k) : (int) $k;
                $out[$clean_k] = self::sanitizeSettingsTreeRecursive($v);
            }
            return $out;
        }
        if (is_string($value)) {
            // Allow newlines (multi-line labels, descriptions); strip tags.
            return sanitize_textarea_field($value);
        }
        // Numbers, bools, null pass through untouched.
        return $value;
    }

    /**
     * AJAX: Get global header settings
     */
    public function ajaxGetGlobalHeader(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $settings = Config::get('instinctor_global_header', null);
        
        if (empty($settings)) {
            wp_send_json_error('No global header saved');
        }
        
        wp_send_json_success(['settings' => $settings]);
    }

    /**
     * AJAX: Save global footer settings
     */
    public function ajaxSaveGlobalFooter(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- raw JSON; wp_unslash + json_decode below; structure validated post-decode
        $settings = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '';

        if (empty($settings)) {
            wp_send_json_error('Missing settings');
        }

        $decoded = json_decode((string) $settings, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            wp_send_json_error('Invalid JSON');
        }

        // Sanitize every leaf string before persistence (see header save above).
        $decoded = self::sanitizeSettingsTreeRecursive($decoded);

        Config::set('instinctor_global_footer', $decoded);

        wp_send_json_success(['message' => 'Global footer saved']);
    }

    /**
     * AJAX: Get global footer settings
     */
    public function ajaxGetGlobalFooter(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $settings = Config::get('instinctor_global_footer', null);
        
        if (empty($settings)) {
            wp_send_json_error('No global footer saved');
        }
        
        wp_send_json_success(['settings' => $settings]);
    }

    /**
     * AJAX: Get card templates list
     */
    public function ajaxGetCardTemplates(): void
    {
        check_ajax_referer('instinctor_card_designer', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        wp_send_json_success([
            'templates' => $this->getCardTemplatesList()
        ]);
    }

    /**
     * AJAX: Save translation
     */

    /**
     * v1.7.0 - Bulk product actions
     */
    public function ajaxBulkProducts(): void
    {
        check_ajax_referer('instinctor_bulk', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $action = sanitize_text_field($_POST['bulk_action'] ?? '');
        $ids = array_map('intval', $_POST['ids'] ?? []);
        
        if (empty($ids)) {
            wp_send_json_error(['message' => 'No products selected']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        $count = count($ids);
        $idList = implode(',', $ids);
        
        switch ($action) {
            case 'publish':
                DB::query("UPDATE $table SET status = 'published' WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count products published"]);
                break;
                
            case 'draft':
                DB::query("UPDATE $table SET status = 'draft' WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count products set to draft"]);
                break;
            
            case 'trash':
                DB::query("UPDATE $table SET status = 'trash' WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count products moved to trash"]);
                break;
            
            case 'restore':
                DB::query("UPDATE $table SET status = 'published' WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count products restored"]);
                break;
                
            case 'delete':
                DB::query("DELETE FROM $table WHERE id IN ($idList)");
                // Also clean up category relationships
                DB::query("DELETE FROM {$p}instinctor_product_categories WHERE product_id IN ($idList)");
                wp_send_json_success(['message' => "$count products permanently deleted"]);
                break;
            
            case 'duplicate':
                $duplicated = 0;
                foreach ($ids as $id) {
                    $product = DB::get_row(DB::prepare("SELECT * FROM $table WHERE id = %d", $id), ARRAY_A);
                    if ($product) {
                        unset($product['id']);
                        $product['name'] = $product['name'] . ' (Copy)';
                        $product['slug'] = $product['slug'] . '-copy-' . time();
                        $product['status'] = 'draft';
                        $product['created_at'] = current_time('mysql');
                        $product['updated_at'] = current_time('mysql');
                        DB::insert($table, $product);
                        $duplicated++;
                    }
                }
                wp_send_json_success(['message' => "$duplicated products duplicated"]);
                break;
                
            default:
                wp_send_json_error(['message' => 'Invalid action']);
        }
    }

    /**
     * Empty trash for products or categories
     */
    public function ajaxEmptyTrash(): void
    {
        check_ajax_referer('instinctor_bulk', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $type = sanitize_text_field($_POST['type'] ?? '');
        
        
        if ($type === 'products') {
            // Get trashed product IDs first
            $trashedIds = DB::get_col("SELECT id FROM {$p}instinctor_products WHERE status = 'trash'");
            if (!empty($trashedIds)) {
                $idList = implode(',', array_map('intval', $trashedIds));
                DB::query("DELETE FROM {$p}instinctor_products WHERE status = 'trash'");
                DB::query("DELETE FROM {$p}instinctor_product_categories WHERE product_id IN ($idList)");
            }
            wp_send_json_success(['message' => 'Product trash emptied']);
        } elseif ($type === 'categories') {
            DB::query("DELETE FROM {$p}instinctor_categories WHERE status = 'trash'");
            wp_send_json_success(['message' => 'Category trash emptied']);
        } else {
            wp_send_json_error(['message' => 'Invalid type']);
        }
    }

    /**
     * Bulk actions for categories
     */
    public function ajaxBulkCategories(): void
    {
        check_ajax_referer('instinctor_bulk', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $action = sanitize_text_field($_POST['bulk_action'] ?? '');
        $ids = array_map('intval', $_POST['ids'] ?? []);
        
        if (empty($ids)) {
            wp_send_json_error(['message' => 'No categories selected']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        $count = count($ids);
        $idList = implode(',', $ids);
        
        switch ($action) {
            case 'trash':
                DB::query("UPDATE $table SET status = 'trash' WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count categories moved to trash"]);
                break;
            
            case 'restore':
                DB::query("UPDATE $table SET status = 'active' WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count categories restored"]);
                break;
                
            case 'delete':
                // First remove from product_categories
                DB::query("DELETE FROM {$p}instinctor_product_categories WHERE category_id IN ($idList)");
                // Then delete the categories
                DB::query("DELETE FROM $table WHERE id IN ($idList)");
                wp_send_json_success(['message' => "$count categories permanently deleted"]);
                break;
                
            default:
                wp_send_json_error(['message' => 'Invalid action']);
        }
    }

    /**
     * v1.7.0 - Inline edit product field
     */
    public function ajaxInlineEditProduct(): void
    {
        check_ajax_referer('instinctor_inline', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        $field = sanitize_text_field($_POST['field'] ?? '');
        $value = sanitize_text_field($_POST['value'] ?? '');
        
        if (!$id || !$field) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        
        $allowed_fields = ['price', 'stock_quantity', 'sale_price', 'sku'];
        if (!in_array($field, $allowed_fields)) {
            wp_send_json_error(['message' => 'Invalid field']);
        }
        
        // Map field names
        $db_field = $field === 'stock' ? 'stock_quantity' : $field;
        
        DB::update($table, [$db_field => $value], ['id' => $id]);
        
        wp_send_json_success(['message' => 'Updated']);
    }

    /**
     * v1.7.0 - Update product status inline
     */
    public function ajaxUpdateProductStatus(): void
    {
        check_ajax_referer('instinctor_inline', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        $status = sanitize_text_field($_POST['status'] ?? '');
        
        if (!$id || !in_array($status, ['published', 'draft'])) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        DB::update(DB::prefix() . 'instinctor_products', ['status' => $status], ['id' => $id]);
        
        wp_send_json_success(['message' => 'Status updated']);
    }

    /**
     * v1.7.0 - Global search
     */
    public function ajaxGlobalSearch(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        $p = DB::prefix();
        $query = sanitize_text_field($_POST['query'] ?? '');
        
        if (strlen($query) < 2) {
            wp_send_json_success(['html' => '']);
        }
        
        $like = '%' . DB::esc_like($query) . '%';
        $html = '';
        
        // Search products
        $products = DB::get_results(DB::prepare(
            "SELECT id, name FROM {$p}instinctor_products WHERE name LIKE %s OR sku LIKE %s LIMIT 5",
            $like, $like
        ));
        
        foreach ($products as $prod) {
            $url = admin_url('admin.php?page=instinctor&workspace=store&product=' . $prod->id);
            $html .= '<a href="' . esc_url($url) . '" class="search-result-item"><span class="search-result-type">Product</span>' . esc_html($prod->name) . '</a>';
        }
        
        // Search orders
        $orders = DB::get_results(DB::prepare(
            "SELECT o.id, o.order_number, c.first_name, c.last_name 
             FROM {$p}instinctor_orders o
             LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
             WHERE o.order_number LIKE %s OR c.first_name LIKE %s OR c.last_name LIKE %s OR c.email LIKE %s
             LIMIT 5",
            $like, $like, $like, $like
        ));
        
        foreach ($orders as $o) {
            $url = admin_url('admin.php?page=instinctor-orders&action=view&id=' . $o->id);
            $name = trim($o->first_name . ' ' . $o->last_name) ?: 'Guest';
            $html .= '<a href="' . esc_url($url) . '" class="search-result-item"><span class="search-result-type">Order</span>#' . esc_html($o->order_number) . ' - ' . esc_html($name) . '</a>';
        }
        
        // Search customers
        $customers = DB::get_results(DB::prepare(
            "SELECT id, first_name, last_name, email FROM {$p}instinctor_customers 
             WHERE first_name LIKE %s OR last_name LIKE %s OR email LIKE %s LIMIT 5",
            $like, $like, $like
        ));
        
        foreach ($customers as $c) {
            $url = admin_url('admin.php?page=instinctor-customers&action=view&id=' . $c->id);
            $name = trim($c->first_name . ' ' . $c->last_name) ?: $c->email;
            $html .= '<a href="' . esc_url($url) . '" class="search-result-item"><span class="search-result-type">Customer</span>' . esc_html($name) . '</a>';
        }
        
        if (empty($html)) {
            $html = '<div class="search-no-results">No results found</div>';
        }
        
        wp_send_json_success(['html' => $html]);
    }

    /**
     * v1.7.0 - Bulk order actions
     */
    public function ajaxBulkOrders(): void
    {
        check_ajax_referer('instinctor_bulk', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $action = sanitize_text_field($_POST['bulk_action'] ?? '');
        $ids = array_map('intval', $_POST['ids'] ?? []);
        
        if (empty($ids)) {
            wp_send_json_error(['message' => 'No orders selected']);
        }
        
        $table = DB::prefix() . 'instinctor_orders';
        $count = count($ids);
        
        $valid_statuses = ['pending', 'processing', 'shipped', 'completed', 'cancelled', 'on-hold', 'refunded'];
        
        if (in_array($action, $valid_statuses)) {
            DB::query("UPDATE $table SET status = '$action' WHERE id IN (" . implode(',', $ids) . ")");
            wp_send_json_success(['message' => "$count orders updated to $action"]);
        } else {
            wp_send_json_error(['message' => 'Invalid action']);
        }
    }

    /**
     * Return orders list + status counts for dynamic reload
     */
    public function ajaxGetOrdersList(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');

        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $p = DB::prefix();
        $batch   = 50;
        $offset  = max(0, intval($_POST['offset'] ?? 0));
        $status  = sanitize_text_field($_POST['status'] ?? '');
        $search  = sanitize_text_field($_POST['search'] ?? '');
        $sortBy  = sanitize_text_field($_POST['sort_by'] ?? 'date');
        $sortDir = strtoupper(sanitize_text_field($_POST['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
        $allowed = ['pending', 'processing', 'shipped', 'completed', 'cancelled', 'refunded', 'on-hold'];

        $sortMap = [
            'date'     => 'o.created_at',
            'customer' => 'c.last_name',
            'status'   => 'o.status',
            'total'    => 'o.total',
        ];
        $orderClause = ($sortMap[$sortBy] ?? 'o.created_at') . ' ' . $sortDir;

        $conditions = [];
        if ($status && in_array($status, $allowed, true)) {
            $conditions[] = DB::prepare('o.status = %s', $status);
        }
        if ($search !== '') {
            $like = '%' . DB::esc_like($search) . '%';
            $conditions[] = DB::prepare(
                '(o.order_number LIKE %s OR c.email LIKE %s OR c.first_name LIKE %s OR c.last_name LIKE %s)',
                $like, $like, $like, $like
            );
        }
        $where = $conditions ? ' WHERE ' . implode(' AND ', $conditions) : '';

        $orders = DB::get_results(DB::prepare(
            "SELECT o.id, o.order_number, o.status, o.total, o.created_at, o.customer_id,
                    c.email AS customer_email, c.first_name, c.last_name
             FROM {$p}instinctor_orders o
             LEFT JOIN {$p}instinctor_customers c ON o.customer_id = c.id
             {$where}
             ORDER BY {$orderClause}
             LIMIT %d OFFSET %d",
            $batch + 1,  // fetch one extra to detect has_more
            $offset
        ));

        $has_more = count($orders) > $batch;
        if ($has_more) array_pop($orders);

        $currency = Config::get('instinctor_currency_symbol', '$');

        $items = [];
        foreach ($orders as $order) {
            $name = trim(($order->first_name ?: '') . ' ' . ($order->last_name ?: ''));
            if (empty($name)) $name = $order->customer_email ?: 'Guest';
            $items[] = [
                'id'           => (int)$order->id,
                'order_number' => $order->order_number,
                'status'       => $order->status,
                'total'        => $currency . number_format((float)$order->total, 2),
                'customer'     => $name,
                'customer_id'  => (int)$order->customer_id,
                'email'        => $order->customer_email ?: '',
                'date'         => date('M j', strtotime($order->created_at)),
                'time_ago'     => human_time_diff(strtotime($order->created_at), current_time('timestamp')) . ' ago',
            ];
        }

        $response = [
            'orders'   => $items,
            'has_more' => $has_more,
            'offset'   => $offset,
            'currency' => $currency,
        ];

        // Only compute counts and finance on first load (offset 0) — avoid repeated heavy queries
        if ($offset === 0) {
            $response['status_counts'] = [
                'all'        => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders"),
                'pending'    => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE status = 'pending'"),
                'processing' => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE status = 'processing'"),
                'completed'  => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE status = 'completed'"),
                'shipped'    => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE status = 'shipped'"),
                'cancelled'  => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE status = 'cancelled'"),
                'refunded'   => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE status = 'refunded'"),
            ];
            $response['finance'] = [
                'today_revenue' => (float)DB::get_var("SELECT COALESCE(SUM(total),0) FROM {$p}instinctor_orders WHERE status IN ('completed','processing') AND DATE(created_at)=CURDATE()"),
                'today_orders'  => (int)DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_orders WHERE DATE(created_at)=CURDATE()"),
                'month_revenue' => (float)DB::get_var("SELECT COALESCE(SUM(total),0) FROM {$p}instinctor_orders WHERE status IN ('completed','processing') AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"),
            ];
        }

        wp_send_json_success($response);
    }

    /**
     * v1.7.0 - Reorder categories
     */
    public function ajaxReorderCategories(): void
    {
        check_ajax_referer('instinctor_reorder', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $order = array_map('intval', $_POST['order'] ?? []);
        
        if (empty($order)) {
            wp_send_json_error(['message' => 'No order provided']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        
        foreach ($order as $position => $id) {
            DB::update(
                $table,
                ['sort_order' => $position],
                ['id' => $id],
                ['%d'],
                ['%d']
            );
        }
        
        wp_send_json_success(['message' => 'Order saved']);
    }

    /**
     * v1.7.0 - Generic inline update
     */
    public function ajaxInlineUpdate(): void
    {
        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'instinctor_admin')) {
            wp_send_json_error(['message' => 'Invalid nonce']);
        }
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        $field = sanitize_text_field($_POST['field'] ?? '');
        $value = sanitize_text_field($_POST['value'] ?? '');
        
        if (!$id || !$field) {
            wp_send_json_error(['message' => 'Missing parameters']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        $display = $value;
        
        // Map field to column
        $allowed_fields = [
            'price' => ['column' => 'price', 'format' => '%f'],
            'stock' => ['column' => 'stock_quantity', 'format' => '%d'],
        ];
        
        if (!isset($allowed_fields[$field])) {
            wp_send_json_error(['message' => 'Invalid field']);
        }
        
        $column = $allowed_fields[$field]['column'];
        $format = $allowed_fields[$field]['format'];
        
        $result = DB::update(
            $table,
            [$column => $value],
            ['id' => $id],
            [$format],
            ['%d']
        );
        
        if ($result !== false) {
            // Format display value
            if ($field === 'price') {
                $currency = \Instinctor\Services\CurrencyService::getSymbol();
                $display = $currency . number_format((float)$value, 2);
            }
            
            wp_send_json_success(['display' => $display, 'value' => $value]);
        } else {
            wp_send_json_error(['message' => 'Update failed']);
        }
    }

    /**
     * Create a new subscription via AJAX
     */
    public function ajaxCreateSubscription(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $table = DB::prefix() . 'instinctor_subscriptions';

        $customer_id = intval($_POST['customer_id'] ?? 0);
        $product_id = intval($_POST['product_id'] ?? 0);
        $price = floatval($_POST['price'] ?? 0);
        $period = sanitize_text_field($_POST['period'] ?? 'monthly');

        if (!$customer_id || $price <= 0) {
            wp_send_json_error(['message' => 'Customer and price are required']);
        }

        // Calculate next payment date
        $period_end = new \DateTime();
        switch ($period) {
            case 'weekly':
                $period_end->modify('+1 week');
                break;
            case 'yearly':
                $period_end->modify('+1 year');
                break;
            default:
                $period_end->modify('+1 month');
        }

        $result = DB::insert($table, [
            'customer_id' => $customer_id,
            'product_id' => $product_id ?: null,
            'price' => $price,
            'period' => $period,
            'status' => 'active',
            'current_period_start' => current_time('mysql'),
            'current_period_end' => $period_end->format('Y-m-d H:i:s'),
            'created_at' => current_time('mysql')
        ]);

        if ($result) {
            wp_send_json_success(['subscription_id' => DB::insert_id()]);
        } else {
            wp_send_json_error(['message' => 'Failed to create subscription']);
        }
    }

    /**
     * Update subscription status via AJAX
     */
    public function ajaxUpdateSubscriptionStatus(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $subscription_id = intval($_POST['subscription_id'] ?? 0);
        $status = sanitize_text_field($_POST['status'] ?? '');

        if (!$subscription_id || !in_array($status, ['active', 'paused', 'cancelled', 'expired'])) {
            wp_send_json_error(['message' => 'Invalid subscription or status']);
        }

        
        // Get subscription to check payment method
        $subscription = DB::get_row(DB::prepare(
            "SELECT * FROM {$p}instinctor_subscriptions WHERE id = %d",
            $subscription_id
        ));

        if (!$subscription) {
            wp_send_json_error(['message' => 'Subscription not found']);
        }

        $isStripe = !empty($subscription->external_id) && strpos($subscription->external_id, 'sub_') === 0;

        $result = ['success' => false];

        if ($isStripe) {
            $stripeService = \Instinctor\Services\SubscriptionService::instance();
            
            switch ($status) {
                case 'paused':
                    $result = $stripeService->pauseSubscription($subscription_id);
                    break;
                case 'active':
                    $result = $stripeService->resumeSubscription($subscription_id);
                    break;
                case 'cancelled':
                    $settings = Config::get('instinctor_subscription_settings', []);
                    $immediately = !empty($settings['cancel_immediately']);
                    $result = $stripeService->cancelSubscription($subscription_id, $immediately);
                    break;
            }
        } else {
            // Local-only subscription (demo mode)
            $update_data = ['status' => $status];
            
            if ($status === 'active') {
                $period = $subscription->period ?? 'monthly';
                $period_end = new \DateTime();
                switch ($period) {
                    case 'weekly': $period_end->modify('+1 week'); break;
                    case 'yearly': $period_end->modify('+1 year'); break;
                    default: $period_end->modify('+1 month');
                }
                $update_data['current_period_start'] = current_time('mysql');
                $update_data['current_period_end'] = $period_end->format('Y-m-d H:i:s');
            } elseif ($status === 'cancelled') {
                $update_data['canceled_at'] = current_time('mysql');
            }
            
            $dbResult = DB::update(
                DB::prefix() . 'instinctor_subscriptions',
                $update_data,
                ['id' => $subscription_id]
            );
            $result = ['success' => $dbResult !== false];
        }

        if (!empty($result['success'])) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => $result['error'] ?? 'Failed to update subscription']);
        }
    }

    /**
     * Save subscription settings via AJAX
     */
    public function ajaxSaveSubscriptionSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        // Parse the serialized form data
        $formData = [];
        if (!empty($_POST['settings'])) {
            parse_str($_POST['settings'], $formData);
        }

        $settings = [
            'allow_pause' => !empty($formData['allow_pause']),
            'pause_limit' => intval($formData['pause_limit'] ?? 2),
            'cancel_immediately' => !empty($formData['cancel_immediately']),
            'prorate_upgrades' => !empty($formData['prorate_upgrades']),
            'failed_payment_retries' => intval($formData['failed_payment_retries'] ?? 3),
            'dunning_emails' => !empty($formData['dunning_emails']),
            'renewal_reminder_days' => intval($formData['renewal_reminder_days'] ?? 3)
        ];

        Config::set('instinctor_subscription_settings', $settings);
        
        wp_send_json_success(['message' => 'Settings saved']);
    }

    // =========================================================================
    // INQUIRIES (form submissions) WORKSPACE HANDLER
    // =========================================================================

    public function ajaxGetFormSubmissions(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        $p = DB::prefix();
        $t = $p . 'instinctor_form_submissions';
        $submissions = [];
        if (DB::get_var(DB::prepare("SHOW TABLES LIKE %s", $t)) === $t) {
            $rows = DB::get_results("SELECT id, form_name, data, created_at, is_read FROM {$t} ORDER BY id DESC LIMIT 500");
            foreach ($rows as $r) {
                $submissions[] = [
                    'id'         => (int) $r->id,
                    'form_name'  => (string) $r->form_name,
                    'created_at' => (string) $r->created_at,
                    'is_read'    => (int) $r->is_read,
                    'fields'     => json_decode($r->data, true) ?: [],
                ];
            }
        }
        wp_send_json_success(['submissions' => $submissions, 'count' => count($submissions)]);
    }

    // =========================================================================
    // BOOKINGS WORKSPACE HANDLERS
    // =========================================================================

    public function ajaxGetBookingsList(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $bt = DB::prefix() . 'instinctor_bookings';
        $ct = DB::prefix() . 'instinctor_customers';
        $st = DB::prefix() . 'instinctor_services';

        $status = sanitize_text_field($_POST['status'] ?? 'upcoming');
        $search = sanitize_text_field($_POST['search'] ?? '');
        $date   = sanitize_text_field($_POST['date']   ?? '');

        $where  = ['1=1'];
        $params = [];

        if ($status === 'upcoming') {
            $where[]  = "b.start_time >= %s AND b.status IN ('pending','confirmed')";
            $params[] = current_time('mysql');
        } elseif ($status !== 'all') {
            $where[]  = 'b.status = %s';
            $params[] = $status;
        }

        if ($date) {
            $where[]  = 'DATE(b.start_time) = %s';
            $params[] = $date;
        }

        if ($search) {
            $like    = '%' . DB::esc_like($search) . '%';
            $where[] = '(c.first_name LIKE %s OR c.last_name LIKE %s OR c.email LIKE %s OR sv.name LIKE %s)';
            $params  = array_merge($params, [$like, $like, $like, $like]);
        }

        $sql = "SELECT b.id, b.status, b.start_time, b.end_time, b.order_id,
                       CONCAT(COALESCE(c.first_name,''), ' ', COALESCE(c.last_name,'')) AS customer_name,
                       sv.name AS service_name
                FROM {$bt} b
                LEFT JOIN {$ct} c  ON b.customer_id = c.id
                LEFT JOIN {$st} sv ON b.product_id  = sv.id
                WHERE " . implode(' AND ', $where) . "
                ORDER BY b.start_time ASC
                LIMIT 200";

        $rows = $params
            ? DB::get_results(DB::prepare($sql, ...$params), ARRAY_A) // phpcs:ignore WordPress.DB.PreparedSQL
            : DB::get_results($sql, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL

        $bookings = array_map(function ($r) {
            $dt = strtotime($r['start_time']);
            return [
                'id'            => (int) $r['id'],
                'status'        => $r['status'],
                'date_label'    => date_i18n('M j', $dt),
                'time_label'    => date_i18n('g:i a', $dt),
                'customer_name' => trim($r['customer_name']) ?: 'Guest',
                'service_name'  => $r['service_name'] ?? '',
                'order_id'      => $r['order_id'],
            ];
        }, $rows ?: []);

        $today   = current_time('Y-m-d');
        $weekEnd = date('Y-m-d', strtotime('+7 days'));
        $stats = [
            'today'    => (int) DB::get_var(DB::prepare("SELECT COUNT(*) FROM {$bt} WHERE DATE(start_time) = %s", $today)),
            'pending'  => (int) DB::get_var("SELECT COUNT(*) FROM {$bt} WHERE status = 'pending'"),
            'week'     => (int) DB::get_var(DB::prepare("SELECT COUNT(*) FROM {$bt} WHERE start_time BETWEEN %s AND %s AND status IN ('pending','confirmed')", current_time('mysql'), $weekEnd . ' 23:59:59')),
            'services' => (int) DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_services WHERE status = 'active'"),
        ];

        wp_send_json_success(['bookings' => $bookings, 'stats' => $stats]);
    }

    public function ajaxGetBookingsStats(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $bt      = DB::prefix() . 'instinctor_bookings';
        $today   = current_time('Y-m-d');
        $weekEnd = date('Y-m-d', strtotime('+7 days'));
        $monthStart = date('Y-m-01');
        $monthEnd   = date('Y-m-t');
        wp_send_json_success([
            'today'   => (int) DB::get_var(DB::prepare("SELECT COUNT(*) FROM {$bt} WHERE DATE(start_time) = %s", $today)),
            'pending' => (int) DB::get_var("SELECT COUNT(*) FROM {$bt} WHERE status = 'pending'"),
            'week'    => (int) DB::get_var(DB::prepare("SELECT COUNT(*) FROM {$bt} WHERE start_time BETWEEN %s AND %s AND status IN ('pending','confirmed')", current_time('mysql'), $weekEnd . ' 23:59:59')),
            'month'   => (int) DB::get_var(DB::prepare("SELECT COUNT(*) FROM {$bt} WHERE DATE(start_time) BETWEEN %s AND %s", $monthStart, $monthEnd)),
        ]);
    }

    public function ajaxGetServices(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $rows = DB::get_results(
            "SELECT id, name, description, duration, buffer_before, buffer_after, price, capacity, color, availability, status
             FROM {$p}instinctor_services
             ORDER BY sort_order ASC, name ASC",
            ARRAY_A
        ) ?: [];

        $services = array_map(function ($r) {
            return [
                'id'            => (int) $r['id'],
                'name'          => $r['name'],
                'description'   => $r['description'] ?? '',
                'duration'      => (int) $r['duration'],
                'buffer_before' => (int) $r['buffer_before'],
                'buffer_after'  => (int) $r['buffer_after'],
                'price'         => $r['price'],
                'capacity'      => (int) $r['capacity'],
                'color'         => $r['color'] ?? '#555555',
                'availability'  => $r['availability'] ? json_decode($r['availability'], true) : null,
                'status'        => $r['status'],
            ];
        }, $rows);

        wp_send_json_success(['services' => $services]);
    }

    public function ajaxSaveService(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $id   = intval($_POST['id'] ?? 0);
        $name = sanitize_text_field($_POST['name'] ?? '');

        if (!$name) {
            wp_send_json_error('Service name is required');
        }

        // Parse availability JSON from POST
        $availRaw = stripslashes(sanitize_text_field(wp_unslash($_POST['availability'] ?? '')));
        $availData = $availRaw ? json_decode($availRaw, true) : null;
        $availability = null;
        if ($availData) {
            $days = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
            $sanitized = [];
            foreach ($days as $day) {
                if (isset($availData[$day])) {
                    $d = $availData[$day];
                    $sanitized[$day] = [
                        'open' => !empty($d['open']),
                        'from' => preg_match('/^\d{2}:\d{2}$/', $d['from'] ?? '') ? $d['from'] : '09:00',
                        'to'   => preg_match('/^\d{2}:\d{2}$/', $d['to']   ?? '') ? $d['to']   : '17:00',
                    ];
                }
            }
            if ($sanitized) $availability = $sanitized;
        }

        $data = [
            'name'          => $name,
            'description'   => sanitize_textarea_field($_POST['description'] ?? ''),
            'duration'      => max(5, intval($_POST['duration'] ?? 60)),
            'buffer_before' => max(0, intval($_POST['buffer_before'] ?? 0)),
            'buffer_after'  => max(0, intval($_POST['buffer_after'] ?? 15)),
            'price'         => floatval($_POST['price'] ?? 0),
            'deposit'       => max(0, floatval($_POST['deposit'] ?? 0)),
            'capacity'      => max(1, intval($_POST['capacity'] ?? 1)),
            'color'         => sanitize_hex_color($_POST['color'] ?? '#555555') ?: '#555555',
            'availability'  => $availability ? wp_json_encode($availability) : null,
            'status'        => in_array(($_inStatus = sanitize_text_field(wp_unslash($_POST['status'] ?? ''))), ['active', 'inactive'], true) ? $_inStatus : 'active',
            'updated_at'    => current_time('mysql'),
        ];

        if ($id) {
            DB::update(DB::prefix() . 'instinctor_services', $data, ['id' => $id]);
            wp_send_json_success(['id' => $id]);
        } else {
            $data['created_at'] = current_time('mysql');
            DB::insert(DB::prefix() . 'instinctor_services', $data);
            wp_send_json_success(['id' => DB::insert_id()]);
        }
    }

    public function ajaxDeleteService(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $id = intval($_POST['id'] ?? 0);
        if (!$id) {
            wp_send_json_error('Invalid ID');
        }
        DB::delete(DB::prefix() . 'instinctor_services', ['id' => $id]);
        wp_send_json_success();
    }

    // ===== Staff / Team (Phase 1) =============================================

    /** Weekday index (0=Sun) -> day key, for hours (de)serialisation. */
    private static $staffDayKeys = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'];

    public function ajaxGetStaff(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $p = DB::prefix();

        $rows = DB::get_results(
            "SELECT id, name, title, bio, photo_url, calendar_color, commission_rate, bookable, status, sort_order
             FROM {$p}instinctor_staff ORDER BY sort_order ASC, name ASC",
            ARRAY_A
        ) ?: [];

        $hoursByStaff = [];
        foreach (DB::get_results("SELECT staff_id, day_of_week, start_time, end_time FROM {$p}instinctor_staff_hours", ARRAY_A) ?: [] as $h) {
            $day = self::$staffDayKeys[(int) $h['day_of_week']] ?? null;
            if ($day) {
                $hoursByStaff[(int) $h['staff_id']][$day] = [
                    'open' => true,
                    'from' => substr((string) $h['start_time'], 0, 5),
                    'to'   => substr((string) $h['end_time'], 0, 5),
                ];
            }
        }

        $svcByStaff = [];
        foreach (DB::get_results("SELECT staff_id, service_id FROM {$p}instinctor_service_staff", ARRAY_A) ?: [] as $r) {
            $svcByStaff[(int) $r['staff_id']][] = (int) $r['service_id'];
        }

        $staff = array_map(function ($r) use ($hoursByStaff, $svcByStaff) {
            $id = (int) $r['id'];
            return [
                'id'              => $id,
                'name'            => $r['name'],
                'title'           => $r['title'] ?? '',
                'bio'             => $r['bio'] ?? '',
                'photo_url'       => $r['photo_url'] ?? '',
                'calendar_color'  => $r['calendar_color'] ?: '#7E946C',
                'commission_rate' => (float) $r['commission_rate'],
                'bookable'        => (int) $r['bookable'] === 1,
                'status'          => $r['status'],
                'hours'           => $hoursByStaff[$id] ?? null,
                'service_ids'     => $svcByStaff[$id] ?? [],
            ];
        }, $rows);

        $svcs = DB::get_results("SELECT id, name FROM {$p}instinctor_services WHERE status = 'active' ORDER BY sort_order ASC, name ASC", ARRAY_A) ?: [];

        wp_send_json_success([
            'staff'    => $staff,
            'services' => array_map(fn($s) => ['id' => (int) $s['id'], 'name' => $s['name']], $svcs),
        ]);
    }

    public function ajaxSaveStaff(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $p = DB::prefix();

        $id   = intval($_POST['id'] ?? 0);
        $name = sanitize_text_field(wp_unslash($_POST['name'] ?? ''));
        if (!$name) {
            wp_send_json_error('Staff name is required');
        }

        $data = [
            'name'            => $name,
            'title'           => sanitize_text_field(wp_unslash($_POST['title'] ?? '')),
            'bio'             => sanitize_textarea_field(wp_unslash($_POST['bio'] ?? '')),
            'photo_url'       => esc_url_raw(wp_unslash($_POST['photo_url'] ?? '')),
            'calendar_color'  => sanitize_hex_color(wp_unslash($_POST['calendar_color'] ?? '#7E946C')) ?: '#7E946C',
            'commission_rate' => max(0, min(100, floatval($_POST['commission_rate'] ?? 0))),
            'bookable'        => !empty($_POST['bookable']) && $_POST['bookable'] !== '0' ? 1 : 0,
            'status'          => in_array(($st = sanitize_text_field(wp_unslash($_POST['status'] ?? ''))), ['active', 'inactive'], true) ? $st : 'active',
            'updated_at'      => current_time('mysql'),
        ];

        if ($id) {
            DB::update($p . 'instinctor_staff', $data, ['id' => $id]);
        } else {
            $data['created_at'] = current_time('mysql');
            DB::insert($p . 'instinctor_staff', $data);
            $id = DB::insert_id();
        }
        if (!$id) {
            wp_send_json_error('Failed to save staff');
        }

        // Weekly hours — replace the whole set when the field is provided.
        if (isset($_POST['hours'])) {
            $hoursRaw  = stripslashes(sanitize_textarea_field(wp_unslash($_POST['hours'])));
            $hoursData = $hoursRaw ? json_decode($hoursRaw, true) : null;
            DB::delete($p . 'instinctor_staff_hours', ['staff_id' => $id]);
            if (is_array($hoursData)) {
                $dowMap = array_flip(self::$staffDayKeys);
                foreach ($hoursData as $day => $d) {
                    if (!isset($dowMap[$day]) || empty($d['open'])) continue;
                    $from = preg_match('/^\d{2}:\d{2}$/', $d['from'] ?? '') ? $d['from'] : '09:00';
                    $to   = preg_match('/^\d{2}:\d{2}$/', $d['to']   ?? '') ? $d['to']   : '17:00';
                    DB::insert($p . 'instinctor_staff_hours', [
                        'staff_id'    => $id,
                        'day_of_week' => $dowMap[$day],
                        'start_time'  => $from . ':00',
                        'end_time'    => $to . ':00',
                    ]);
                }
            }
        }

        // Service assignments — replace when the field is provided.
        if (isset($_POST['service_ids'])) {
            $svcRaw = sanitize_text_field(wp_unslash($_POST['service_ids']));
            $svcIds = $svcRaw !== '' ? array_unique(array_filter(array_map('intval', explode(',', $svcRaw)))) : [];
            DB::delete($p . 'instinctor_service_staff', ['staff_id' => $id]);
            foreach ($svcIds as $svcId) {
                if ($svcId > 0) DB::insert($p . 'instinctor_service_staff', ['service_id' => $svcId, 'staff_id' => $id]);
            }
        }

        wp_send_json_success(['id' => $id]);
    }

    public function ajaxDeleteStaff(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $p  = DB::prefix();
        $id = intval($_POST['id'] ?? 0);
        if (!$id) {
            wp_send_json_error('Invalid ID');
        }
        DB::delete($p . 'instinctor_staff', ['id' => $id]);
        DB::delete($p . 'instinctor_staff_hours', ['staff_id' => $id]);
        DB::delete($p . 'instinctor_staff_timeoff', ['staff_id' => $id]);
        DB::delete($p . 'instinctor_service_staff', ['staff_id' => $id]);
        // Keep past bookings, just unassign the deleted staff.
        DB::query(DB::prepare("UPDATE {$p}instinctor_bookings SET staff_id = NULL WHERE staff_id = %d", $id));
        wp_send_json_success();
    }

    // =========================================================================

    /**
     * Get bookings for calendar view
     */
    public function ajaxGetBookingsCalendar(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $start = sanitize_text_field($_POST['start'] ?? date('Y-m-01'));
        $end = sanitize_text_field($_POST['end'] ?? date('Y-m-t'));
        
        $bookingService = \Instinctor\Services\BookingService::instance();
        $events = $bookingService->getBookingsForCalendar($start, $end);
        
        wp_send_json_success($events);
    }

    /**
     * Get single booking details
     */
    public function ajaxGetBooking(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $bookingId = intval($_POST['booking_id'] ?? 0);
        if (!$bookingId) {
            wp_send_json_error(['message' => 'Invalid booking ID']);
        }

        $booking = DB::get_row(DB::prepare(
            "SELECT b.*, sv.name as service_name, c.first_name, c.last_name, c.email, c.phone,
                    st.name as staff_name
             FROM {$p}instinctor_bookings b
             LEFT JOIN {$p}instinctor_services sv ON b.product_id = sv.id
             LEFT JOIN {$p}instinctor_customers c ON b.customer_id = c.id
             LEFT JOIN {$p}instinctor_staff st ON b.staff_id = st.id
             WHERE b.id = %d",
            $bookingId
        ));

        if (!$booking) {
            wp_send_json_error(['message' => 'Booking not found']);
        }

        $customerName = trim($booking->first_name . ' ' . $booking->last_name);
        
        $statusLabels = [
            'pending' => __('Pending', 'instinctor'),
            'confirmed' => __('Confirmed', 'instinctor'),
            'completed' => __('Completed', 'instinctor'),
            'cancelled' => __('Cancelled', 'instinctor'),
            'no_show' => __('No Show', 'instinctor'),
        ];
        
        wp_send_json_success([
            'id' => $booking->id,
            'service_name' => $booking->service_name ?? '',
            'customer_name' => $customerName ?: 'Customer #' . $booking->customer_id,
            'email' => $booking->email,
            'phone' => $booking->phone,
            'date' => date_i18n(\Instinctor\Core\Config::get('date_format', 'Y-m-d'), strtotime($booking->start_time)),
            'time' => date_i18n(\Instinctor\Core\Config::get('time_format', 'H:i'), strtotime($booking->start_time)) . ' - ' . date_i18n(\Instinctor\Core\Config::get('time_format', 'H:i'), strtotime($booking->end_time)),
            'status' => $booking->status,
            'status_label' => $statusLabels[$booking->status] ?? ucfirst($booking->status),
            'note' => $booking->customer_note,
            'order_id' => $booking->order_id,
            'staff_id' => (int) ($booking->staff_id ?? 0),
            'staff_name' => $booking->staff_name ?? null
        ]);
    }

    /**
     * Owner-side staff assignment: assign (or unassign) the specialist handling a
     * booking / reservation / work order / job. Customers never pick staff here
     * (the salon frontend picker is a separate flow); this is the admin override.
     */
    public function ajaxUpdateBookingStaff(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        $p         = DB::prefix();
        $bookingId = intval($_POST['booking_id'] ?? 0);
        $staffId   = intval($_POST['staff_id'] ?? 0); // 0 = unassign
        if (!$bookingId) {
            wp_send_json_error(['message' => 'Invalid booking']);
        }
        $exists = DB::get_var(DB::prepare("SELECT id FROM {$p}instinctor_bookings WHERE id = %d", $bookingId));
        if (!$exists) {
            wp_send_json_error(['message' => 'Booking not found']);
        }
        $staffName = null;
        if ($staffId) {
            $staff = DB::get_row(DB::prepare("SELECT id, name FROM {$p}instinctor_staff WHERE id = %d AND status = 'active'", $staffId));
            if (!$staff) {
                wp_send_json_error(['message' => 'Staff member not found']);
            }
            $staffName = $staff->name;
        }
        DB::update($p . 'instinctor_bookings', ['staff_id' => $staffId ?: null], ['id' => $bookingId]);
        wp_send_json_success(['staff_id' => $staffId, 'staff_name' => $staffName]);
    }

    /**
     * Create a manual booking
     */
    public function ajaxCreateBooking(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $productId = intval($_POST['product_id'] ?? 0);
        $customerEmail = sanitize_email($_POST['customer_email'] ?? '');
        $customerName = sanitize_text_field($_POST['customer_name'] ?? '');
        $customerPhone = sanitize_text_field($_POST['customer_phone'] ?? '');
        $date = sanitize_text_field($_POST['booking_date'] ?? '');
        $time = sanitize_text_field($_POST['booking_time'] ?? '');
        $note = sanitize_textarea_field($_POST['note'] ?? '');

        if (!$productId || !$customerEmail || !$date || !$time) {
            wp_send_json_error(['message' => 'Missing required fields']);
        }

        
        // Get or create customer
        $customer = DB::get_row(DB::prepare(
            "SELECT id FROM {$p}instinctor_customers WHERE email = %s",
            $customerEmail
        ));

        if ($customer) {
            $customerId = $customer->id;
            // Update phone if provided
            if ($customerPhone) {
                DB::update(
                    DB::prefix() . 'instinctor_customers',
                    ['phone' => $customerPhone],
                    ['id' => $customerId]
                );
            }
        } else {
            $nameParts = explode(' ', $customerName, 2);
            DB::insert(DB::prefix() . 'instinctor_customers', [
                'email' => $customerEmail,
                'first_name' => $nameParts[0] ?? '',
                'last_name' => $nameParts[1] ?? '',
                'phone' => $customerPhone,
                'created_at' => current_time('mysql')
            ]);
            $customerId = DB::insert_id();
        }

        $bookingService = \Instinctor\Services\BookingService::instance();
        $result = $bookingService->createBooking([
            'product_id' => $productId,
            'customer_id' => $customerId,
            'date' => $date,
            'time' => $time,
            'note' => $note,
            'created_via' => 'admin'
        ]);

        if ($result['success']) {
            // Mark as confirmed for manual bookings
            DB::update(
                DB::prefix() . 'instinctor_bookings',
                ['status' => 'confirmed'],
                ['id' => $result['booking_id']]
            );
            wp_send_json_success(['booking_id' => $result['booking_id']]);
        } else {
            wp_send_json_error(['message' => $result['error'] ?? 'Failed to create booking']);
        }
    }

    /**
     * Update booking status
     */
    public function ajaxUpdateBookingStatus(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $bookingId = intval($_POST['booking_id'] ?? 0);
        $status = sanitize_text_field($_POST['status'] ?? '');

        if (!$bookingId || !$status) {
            wp_send_json_error(['message' => 'Invalid parameters']);
        }

        $bookingService = \Instinctor\Services\BookingService::instance();
        $result = $bookingService->updateStatus($bookingId, $status);

        if ($result['success']) {
            wp_send_json_success();
        } else {
            wp_send_json_error(['message' => $result['error'] ?? 'Failed to update']);
        }
    }

    /**
     * Save booking settings
     */
    public function ajaxSaveBookingSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        parse_str(sanitize_text_field(wp_unslash($_POST['settings'] ?? '')), $formData);

        $bookingService = \Instinctor\Services\BookingService::instance();
        $currentSettings = $bookingService->getSettings();

        // Process business hours
        $businessHours = [];
        $days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
        foreach ($days as $day) {
            $businessHours[$day] = [
                'enabled' => !empty($formData['business_hours'][$day]['enabled']),
                'start' => sanitize_text_field($formData['business_hours'][$day]['start'] ?? '09:00'),
                'end' => sanitize_text_field($formData['business_hours'][$day]['end'] ?? '17:00')
            ];
        }

        // Process blocked dates
        $blockedDates = [];
        if (!empty($formData['blocked_dates']) && is_array($formData['blocked_dates'])) {
            foreach ($formData['blocked_dates'] as $date) {
                $date = sanitize_text_field($date);
                if ($date && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
                    $blockedDates[] = $date;
                }
            }
        }

        $settings = [
            'default_duration' => intval($formData['default_duration'] ?? 60),
            'buffer_before' => intval($formData['buffer_before'] ?? 0),
            'buffer_after' => intval($formData['buffer_after'] ?? 15),
            'min_advance' => intval($formData['min_advance'] ?? 24),
            'max_advance' => intval($formData['max_advance'] ?? 60),
            'slot_interval' => intval($formData['slot_interval'] ?? 30),
            'allow_cancellation' => !empty($formData['allow_cancellation']),
            'cancellation_deadline' => intval($formData['cancellation_deadline'] ?? 24),
            'require_payment' => !empty($formData['require_payment']),
            'auto_refund_within_window' => !empty($formData['auto_refund_within_window']),
            'send_reminders' => !empty($formData['send_reminders']),
            'reminder_hours' => intval($formData['reminder_hours'] ?? 24),
            'notify_sms' => sanitize_text_field($formData['notify_sms'] ?? ''),
            'notify_customer_sms' => !empty($formData['notify_customer_sms']),
            'business_hours' => $businessHours,
            'blocked_dates' => $blockedDates,
            'timezone' => sanitize_text_field($formData['timezone'] ?? wp_timezone_string())
        ];

        $bookingService->saveSettings($settings);

        wp_send_json_success(['message' => 'Settings saved']);
    }

    // ── Instructor vertical: class schedule CRUD ────────────────────────────

    
    
    /**
     * Handle bug report submission
     */
    
    // =========================================================================
    // IMPULSE WORDPRESS PAGES INTEGRATION
    // =========================================================================
    
    /**
     * Check if a page is a Impulse page
     * Checks both _impulse_enabled meta AND presence of _impulse_elements or _impulse_settings
     */
    public static function isImpulsePage(int $post_id): bool
    {
        if (!$post_id) {
            return false;
        }
        
        // Check explicit flag first
        $enabled = get_post_meta($post_id, '_impulse_enabled', true);
        if ($enabled === '1' || $enabled === 1 || $enabled === true) {
            return true;
        }
        
        // Fallback: check for presence of Impulse elements or settings
        $elements = get_post_meta($post_id, '_impulse_elements', true);
        $settings = get_post_meta($post_id, '_impulse_settings', true);
        
        // If either elements or settings exist, it's a Impulse page
        if (!empty($elements) || !empty($settings)) {
            // Auto-fix: set the enabled flag for future checks
            update_post_meta($post_id, '_impulse_enabled', '1');
            return true;
        }
        
        return false;
    }
    
    /**
     * Redirect standard edit links to Impulse for Impulse-enabled pages
     */
    public function redirectImpulseEdit(): void
    {
        global $pagenow;
        
        if ($pagenow !== 'post.php') {
            return;
        }
        
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $action = isset($_GET['action']) ? sanitize_text_field($_GET['action']) : '';
        
        if (!$post_id || $action !== 'edit') {
            return;
        }
        
        $post = get_post($post_id);
        if (!$post || $post->post_type !== 'page') {
            return;
        }

        // Only redirect users who can actually edit THIS page into the Impulse
        // editor. A user-supplied ?post=<id> must not bypass per-post caps
        // (the generic admin-panel gate is not page-specific). WP.org round 4.
        if (!current_user_can('edit_post', $post_id)) {
            return;
        }

        if (self::isImpulsePage($post_id)) {
            wp_safe_redirect(admin_url('admin.php?page=instinctor&action=edit&page_id=' . $post_id));
            exit;
        }
    }
    
    /**
     * Add "Edit with Impulse" action to Pages list
     */
    public function addImpulseRowAction(array $actions, \WP_Post $post): array
    {
        if ($post->post_type !== 'page') {
            return $actions;
        }
        
        $impulse_url = admin_url('admin.php?page=instinctor&action=edit&page_id=' . $post->ID);
        
        if (self::isImpulsePage($post->ID)) {
            // Replace Edit link with Impulse edit
            $actions['edit'] = '<a href="' . esc_url($impulse_url) . '">Edit</a>';
            // Remove Quick Edit for Impulse pages
            unset($actions['inline hide-if-no-js']);
        } else {
            // Add "Edit with Impulse" option for regular pages
            $actions['impulse'] = '<a href="' . esc_url($impulse_url . '&convert=1') . '">Edit with Impulse</a>';
        }
        
        return $actions;
    }
    
    /**
     * Filter the edit post link for Impulse pages
     */
    public function filterImpulseEditLink(string $link, int $post_id, string $context): string
    {
        $post = get_post($post_id);
        if (!$post || $post->post_type !== 'page') {
            return $link;
        }
        
        if (self::isImpulsePage($post_id)) {
            return admin_url('admin.php?page=instinctor&action=edit&page_id=' . $post_id);
        }
        
        return $link;
    }
    
    /**
     * Disable Gutenberg for Impulse-enabled pages
     */
    public function disableGutenbergForImpulse(bool $use, \WP_Post $post): bool
    {
        if ($post->post_type === 'page' && self::isImpulsePage($post->ID)) {
            return false;
        }
        return $use;
    }
    
    /**
     * Add meta box showing Impulse edit button on page edit screen
     */
    public function addImpulseMetaBox(): void
    {
        add_meta_box(
            'impulse_edit_box',
            'Impulse',
            [$this, 'renderImpulseMetaBox'],
            'page',
            'side',
            'high'
        );
    }
    
    /**
     * Render Impulse meta box content
     */
    public function renderImpulseMetaBox(\WP_Post $post): void
    {
        $impulse_url = admin_url('admin.php?page=instinctor&action=edit&page_id=' . $post->ID);
        
        if (self::isImpulsePage($post->ID)) {
            echo '<p>This page is built with Impulse.</p>';
            echo '<a href="' . esc_url($impulse_url) . '" class="button button-primary">Open in Impulse</a>';
        } else {
            echo '<p>Convert this page to use Impulse page builder.</p>';
            echo '<a href="' . esc_url($impulse_url . '&convert=1') . '" class="button">Edit with Impulse</a>';
        }
    }
    
    // =====================================================================
    // Impulse Products Workspace AJAX Handlers
    // =====================================================================
    
    /**
     * Get products for Impulse workspace
     */
    public function ajaxImpulseGetProducts(): void
    {
        $start_time = microtime(true);
        
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $p = DB::prefix();
        $table = $p . 'instinctor_products';
        
        // Filters
        $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'all';
        $stock_filter = isset($_POST['stock_filter']) ? sanitize_text_field($_POST['stock_filter']) : '';
        $category = isset($_POST['category']) ? intval($_POST['category']) : 0;
        $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
        $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
        $per_page = 50;
        $offset = ($page - 1) * $per_page;
        
        // Build WHERE clause
        $where = ['1=1'];
        $params = [];
        
        // Status filter
        if ($status === 'all') {
            $where[] = "p.status != 'trash'";
        } elseif ($status === 'archived') {
            $where[] = "p.status = 'archived'";
        } elseif ($status === 'published') {
            $where[] = "p.status = 'published'";
        } elseif ($status === 'draft') {
            $where[] = "p.status = 'draft'";
        }
        
        // Product type filter (for bundle tab)
        $type_filter = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : '';
        if (!empty($type_filter)) {
            $where[] = 'p.product_type = %s';
            $params[] = $type_filter;
        }

        // Stock filter
        if ($stock_filter === 'low_stock') {
            $threshold = Config::get('instinctor_low_stock_threshold', 5);
            $where[] = "p.manage_stock = 1 AND p.stock_quantity > 0 AND p.stock_quantity <= %d";
            $params[] = $threshold;
        } elseif ($stock_filter === 'out_of_stock') {
            $where[] = "(p.stock_status = 'outofstock' OR (p.manage_stock = 1 AND p.stock_quantity <= 0))";
        }
        
        // Category filter
        if ($category > 0) {
            $where[] = "p.id IN (SELECT product_id FROM {$p}instinctor_product_categories WHERE category_id = %d)";
            $params[] = $category;
        }
        
        // Search
        if (!empty($search)) {
            $like = '%' . DB::esc_like($search) . '%';
            $where[] = "(p.name LIKE %s OR p.sku LIKE %s)";
            $params[] = $like;
            $params[] = $like;
        }
        
        $where_sql = implode(' AND ', $where);
        
        // Get total count
        $count_sql = "SELECT COUNT(*) FROM $table p WHERE $where_sql";
        if (!empty($params)) {
            $total = DB::get_var(DB::prepare($count_sql, $params));
        } else {
            $total = DB::get_var($count_sql);
        }
        
        // Get products
        $sql = "SELECT p.id, p.name, p.slug, p.product_type, p.status, p.price, p.sale_price, 
                       p.sku, p.stock_quantity, p.stock_status, p.manage_stock, p.featured_image,
                       p.short_description, p.description, p.attributes, p.gallery_images, p.images, p.meta,
                       p.is_featured, p.sort_order, p.created_at, p.updated_at
                FROM $table p 
                WHERE $where_sql 
                ORDER BY p.sort_order ASC, p.updated_at DESC 
                LIMIT %d OFFSET %d";
        
        $params[] = $per_page;
        $params[] = $offset;
        
        $products = DB::get_results(DB::prepare($sql, $params));
        
        // Get all product IDs for batch category fetch
        $product_ids = array_map(function($p) { return $p->id; }, $products);
        $product_categories = [];
        
        if (!empty($product_ids)) {
            $ids_str = implode(',', $product_ids);
            $cat_rows = DB::get_results("
                SELECT product_id, category_id 
                FROM {$p}instinctor_product_categories 
                WHERE product_id IN ($ids_str)
            ");
            foreach ($cat_rows as $row) {
                if (!isset($product_categories[$row->product_id])) {
                    $product_categories[$row->product_id] = [];
                }
                $product_categories[$row->product_id][] = (int) $row->category_id;
            }
        }
        
        // Format products
        $result = [];
        $low_threshold = Config::get('instinctor_low_stock_threshold', 5);
        
        // Batch collect all attachment IDs for a single optimized lookup
        $all_attachment_ids = [];
        foreach ($products as $p) {
            if ($p->featured_image) {
                $all_attachment_ids[] = (int) $p->featured_image;
            }
            if ($p->gallery_images) {
                $gallery_ids = json_decode($p->gallery_images, true) ?: [];
                foreach ($gallery_ids as $img_id) {
                    $all_attachment_ids[] = (int) $img_id;
                }
            }
        }
        
        // Batch fetch all attachment URLs in one query
        $attachment_urls = [];
        if (!empty($all_attachment_ids)) {
            $unique_ids = array_unique($all_attachment_ids);
            $ids_placeholder = implode(',', $unique_ids);
            
            // Try instinctor_images table first (standalone-compatible)
            $imgTable = DB::prefix() . 'instinctor_images';
            $imgRows = DB::get_results(
                "SELECT id, rel_path FROM {$imgTable} WHERE id IN ({$ids_placeholder}) AND parent_id IS NULL"
            );
            $base_url = Storage::baseurl();
            foreach ($imgRows as $row) {
                if (!empty($row->rel_path)) {
                    $attachment_urls[(int)$row->id] = $base_url . '/' . ltrim($row->rel_path, '/');
                }
            }
            
            // Fallback: any IDs not found in instinctor_images — try WP media
            $missing = array_diff($unique_ids, array_keys($attachment_urls));
            if (!empty($missing) && (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress')) {
                global $wpdb;
                $miss_placeholder = implode(',', $missing);
                $attachments = DB::get_results("
                    SELECT p.ID, pm.meta_value as file
                    FROM {$wpdb->posts} p
                    LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attached_file'
                    WHERE p.ID IN ($miss_placeholder)
                ");
                foreach ($attachments as $att) {
                    if ($att->file) {
                        $file_info = pathinfo($att->file);
                        $thumbnail_file = $file_info['dirname'] . '/' . $file_info['filename'] . '-150x150.' . $file_info['extension'];
                        if (Storage::exists($thumbnail_file)) {
                            $attachment_urls[$att->ID] = $base_url . '/' . $thumbnail_file;
                        } else {
                            $attachment_urls[$att->ID] = $base_url . '/' . $att->file;
                        }
                    }
                }
            }
        }
        
        foreach ($products as $p) {
            // Get image URL from batch lookup
            $image_url = '';
            if ($p->featured_image && isset($attachment_urls[$p->featured_image])) {
                $image_url = $attachment_urls[$p->featured_image];
            }
            // Fallback to images JSON field (URL-based)
            if (empty($image_url) && !empty($p->images)) {
                $imagesData = json_decode($p->images, true);
                if (is_array($imagesData) && !empty($imagesData[0]['url'])) {
                    $image_url = $imagesData[0]['url'];
                }
            }
            
            // Determine stock status for display
            $display_stock = 'in_stock';
            if ($p->stock_status === 'outofstock' || ($p->manage_stock && $p->stock_quantity <= 0)) {
                $display_stock = 'out';
            } elseif ($p->manage_stock && $p->stock_quantity <= $low_threshold) {
                $display_stock = 'low';
            }
            
            // Parse meta JSON
            $meta = $p->meta ? json_decode($p->meta, true) : [];
            
            // Parse gallery images from batch lookup
            $gallery = [];
            if ($p->gallery_images) {
                $gallery_ids = json_decode($p->gallery_images, true) ?: [];
                foreach ($gallery_ids as $img_id) {
                    if (isset($attachment_urls[$img_id])) {
                        $gallery[] = ['id' => (int) $img_id, 'url' => $attachment_urls[$img_id]];
                    }
                }
            }
            
            // Parse attributes
            $attributes = $p->attributes ? json_decode($p->attributes, true) : [];
            
            // Parse variations from meta
            $variations = isset($meta['variations']) ? json_decode($meta['variations'], true) : [];
            
            $result[] = [
                'id' => (int) $p->id,
                'name' => $p->name,
                'slug' => $p->slug,
                'type' => $p->product_type,
                'subscription_interval' => $meta['subscription_interval'] ?? 'monthly',
                'subscription_interval_count' => isset($meta['subscription_interval_count']) ? (int)$meta['subscription_interval_count'] : 1,
                'trial_days' => isset($meta['trial_days']) ? (int)$meta['trial_days'] : 0,
                'status' => $p->status,
                'price' => $p->price ? floatval($p->price) : null,
                'sale_price' => $p->sale_price ? floatval($p->sale_price) : null,
                'sku' => $p->sku ?: '',
                'stock' => $p->manage_stock ? (int) $p->stock_quantity : null,
                'stock_status' => $display_stock,
                'image' => $image_url ?: '',
                'featured_image' => $p->featured_image ? (int) $p->featured_image : null,
                'image_fit' => $meta['image_fit'] ?? 'cover',
                'short_description' => $p->short_description ?: '',
                'description' => $p->description ?: '',
                'meta_title' => $meta['meta_title'] ?? '',
                'meta_description' => $meta['meta_description'] ?? '',
                'gallery' => $gallery,
                'attributes' => $attributes,
                'variations' => $variations,
                'categories' => isset($product_categories[$p->id]) ? $product_categories[$p->id] : [],
                'updated_at' => $p->updated_at,
                // Bundle-specific fields (from meta)
                'bundle_type'           => $meta['bundle_type'] ?? 'fixed',
                'bundle_discount_type'  => $meta['bundle_discount_type'] ?? 'percent',
                'bundle_discount_value' => isset($meta['bundle_discount_value']) ? floatval($meta['bundle_discount_value']) : 0,
                'bundle_min'            => isset($meta['bundle_min']) ? intval($meta['bundle_min']) : 0,
                'bundle_max'            => isset($meta['bundle_max']) ? intval($meta['bundle_max']) : 0,
                'bundle_products'       => $meta['bundle_products'] ?? '[]',
                'bundle_product_names'  => $meta['bundle_product_names'] ?? [],
                'bundle_product_images' => $meta['bundle_product_images'] ?? [],
            ];
        }
        
        $elapsed = round((microtime(true) - $start_time) * 1000);
        
        wp_send_json_success([
            'products' => $result,
            'total' => (int) $total,
            'page' => $page,
            'per_page' => $per_page,
            'pages' => ceil($total / $per_page),
            'query_time_ms' => $elapsed
        ]);
    }
    
    /**
     * Get categories for Impulse workspace
     */
    public function ajaxImpulseGetCategories(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $p = DB::prefix();
        $table = $p . 'instinctor_categories';
        
        $categories = DB::get_results("
            SELECT c.id, c.name, c.slug, c.parent_id, c.sort_order,
                   (SELECT COUNT(*) FROM {$p}instinctor_product_categories pc WHERE pc.category_id = c.id) as product_count
            FROM $table c 
            WHERE c.status = 'active'
            ORDER BY c.parent_id ASC, c.sort_order ASC, c.name ASC
        ");
        
        // Build tree structure
        $tree = [];
        $lookup = [];
        
        foreach ($categories as $cat) {
            $lookup[$cat->id] = [
                'id' => (int) $cat->id,
                'name' => $cat->name,
                'slug' => $cat->slug,
                'parent_id' => $cat->parent_id ? (int) $cat->parent_id : null,
                'count' => (int) $cat->product_count,
                'children' => []
            ];
        }
        
        foreach ($lookup as $id => &$cat) {
            if ($cat['parent_id'] && isset($lookup[$cat['parent_id']])) {
                $lookup[$cat['parent_id']]['children'][] = &$cat;
            } else {
                $tree[] = &$cat;
            }
        }
        
        wp_send_json_success(['categories' => $tree]);
    }
    
    /**
     * Create category for Impulse workspace
     */
    public function ajaxImpulseCreateCategory(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
        $parent_id = isset($_POST['parent_id']) ? intval($_POST['parent_id']) : 0;
        
        if (empty($name)) {
            wp_send_json_error(['message' => 'Category name is required']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        
        // Generate slug
        $slug = sanitize_title($name);
        $original_slug = $slug;
        $counter = 1;
        while (DB::get_var(DB::prepare("SELECT id FROM $table WHERE slug = %s", $slug))) {
            $slug = $original_slug . '-' . $counter;
            $counter++;
        }
        
        // Get next sort order
        $max_sort = DB::get_var(DB::prepare(
            "SELECT MAX(sort_order) FROM $table WHERE parent_id = %d OR (parent_id IS NULL AND %d = 0)",
            $parent_id, $parent_id
        ));
        $sort_order = ($max_sort !== null) ? (int)$max_sort + 1 : 0;
        
        // Insert without updated_at (column may not exist in older installations)
        $insert_data = [
            'name' => $name,
            'slug' => $slug,
            'parent_id' => $parent_id > 0 ? $parent_id : null,
            'sort_order' => $sort_order,
            'status' => 'active',
            'created_at' => current_time('mysql')
        ];
        
        $result = DB::insert($table, $insert_data);
        
        if ($result) {
            wp_send_json_success([
                'message' => 'Category created',
                'id' => DB::insert_id(),
                'name' => $name,
                'slug' => $slug
            ]);
        } else {
            // Include database error for debugging
            wp_send_json_error([
                'message' => 'Failed to create category',
                'db_error' => DB::last_error()
            ]);
        }
    }
    
    /**
     * Update category for Impulse workspace
     */
    public function ajaxImpulseUpdateCategory(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
        $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
        
        if (!$id) {
            wp_send_json_error(['message' => 'Category ID is required']);
        }
        
        if (empty($name)) {
            wp_send_json_error(['message' => 'Category name is required']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        
        // Generate new slug
        $slug = sanitize_title($name);
        $original_slug = $slug;
        $counter = 1;
        while (DB::get_var(DB::prepare("SELECT id FROM $table WHERE slug = %s AND id != %d", $slug, $id))) {
            $slug = $original_slug . '-' . $counter;
            $counter++;
        }
        
        $result = DB::update($table, [
            'name' => $name,
            'slug' => $slug,
            'updated_at' => current_time('mysql')
        ], ['id' => $id]);
        
        if ($result !== false) {
            wp_send_json_success([
                'message' => 'Category updated',
                'id' => $id,
                'name' => $name,
                'slug' => $slug
            ]);
        } else {
            wp_send_json_error(['message' => 'Failed to update category']);
        }
    }
    
    /**
     * Delete category for Impulse workspace
     */
    public function ajaxImpulseDeleteCategory(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
        
        if (!$id) {
            wp_send_json_error(['message' => 'Category ID is required']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        $productCatTable = DB::prefix() . 'instinctor_product_categories';
        
        // Check for child categories
        $children = DB::get_var(DB::prepare(
            "SELECT COUNT(*) FROM $table WHERE parent_id = %d AND status = 'active'",
            $id
        ));
        
        if ($children > 0) {
            wp_send_json_error(['message' => 'Cannot delete category with subcategories. Delete subcategories first.']);
        }
        
        // Remove product associations
        DB::delete($productCatTable, ['category_id' => $id]);
        
        // Soft delete the category
        $result = DB::update($table, [
            'status' => 'deleted'
        ], ['id' => $id]);
        
        if ($result !== false) {
            wp_send_json_success(['message' => 'Category deleted']);
        } else {
            wp_send_json_error(['message' => 'Failed to delete category']);
        }
    }
    
    /**
     * Reorder categories for Impulse workspace
     */
    public function ajaxImpulseReorderCategories(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $order = isset($_POST['order']) ? json_decode(sanitize_textarea_field(wp_unslash($_POST['order'])), true) : [];
        
        if (empty($order)) {
            wp_send_json_error(['message' => 'Order data is required']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        
        foreach ($order as $item) {
            if (isset($item['id']) && isset($item['sort_order'])) {
                DB::update($table, [
                    'sort_order' => (int) $item['sort_order']
                ], ['id' => (int) $item['id']]);
            }
        }
        
        wp_send_json_success(['message' => 'Order updated']);
    }
    
    /**
     * Move category to new parent for Impulse workspace
     */
    public function ajaxImpulseMoveCategory(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
        $parent_id = isset($_POST['parent_id']) ? intval($_POST['parent_id']) : 0;
        $target_id = isset($_POST['target_id']) ? intval($_POST['target_id']) : 0;
        $position = isset($_POST['position']) ? sanitize_text_field($_POST['position']) : 'nest';
        
        if (!$id) {
            wp_send_json_error(['message' => 'Category ID is required']);
        }
        
        $table = DB::prefix() . 'instinctor_categories';
        
        // Prevent moving to self
        if ($id === $parent_id) {
            wp_send_json_error(['message' => 'Cannot move category to itself']);
        }
        
        // Prevent moving to own descendant (check recursively)
        if ($parent_id > 0) {
            $check_id = $parent_id;
            $max_depth = 10;
            $depth = 0;
            while ($check_id > 0 && $depth < $max_depth) {
                $ancestor = DB::get_row(DB::prepare(
                    "SELECT id, parent_id FROM $table WHERE id = %d",
                    $check_id
                ));
                if (!$ancestor) break;
                if ($ancestor->id == $id) {
                    wp_send_json_error(['message' => 'Cannot move category to its own descendant']);
                }
                $check_id = $ancestor->parent_id ?: 0;
                $depth++;
            }
        }
        
        // Update parent
        $result = DB::update($table, [
            'parent_id' => $parent_id > 0 ? $parent_id : null
        ], ['id' => $id]);
        
        // If positioning relative to target, update sort order
        if ($position === 'above' || $position === 'below') {
            $target_order = DB::get_var(DB::prepare(
                "SELECT sort_order FROM $table WHERE id = %d",
                $target_id
            ));
            
            if ($target_order !== null) {
                $new_order = ($position === 'above') ? $target_order : $target_order + 1;
                
                // Shift other items
                DB::query(DB::prepare(
                    "UPDATE $table SET sort_order = sort_order + 1 
                     WHERE parent_id " . ($parent_id > 0 ? "= $parent_id" : "IS NULL") . "
                     AND sort_order >= %d AND id != %d",
                    $new_order, $id
                ));
                
                // Set new order for moved item
                DB::update($table, ['sort_order' => $new_order], ['id' => $id]);
            }
        } else {
            // Nest - put at end of children
            $max_order = DB::get_var(DB::prepare(
                "SELECT MAX(sort_order) FROM $table WHERE parent_id " . ($parent_id > 0 ? "= %d" : "IS NULL"),
                $parent_id
            ));
            DB::update($table, ['sort_order' => ($max_order ?? -1) + 1], ['id' => $id]);
        }
        
        if ($result !== false) {
            wp_send_json_success(['message' => 'Category moved']);
        } else {
            wp_send_json_error(['message' => 'Failed to move category', 'db_error' => DB::last_error()]);
        }
    }
    
    /**
     * Get inventory stats for Impulse workspace
     */
    public function ajaxImpulseGetInventoryStats(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        $threshold = Config::get('instinctor_low_stock_threshold', 5);
        
        $total = DB::get_var("SELECT COUNT(*) FROM $table WHERE status != 'trash'");
        
        $in_stock = DB::get_var("
            SELECT COUNT(*) FROM $table 
            WHERE status != 'trash' 
            AND (stock_status = 'instock' OR (manage_stock = 0) OR (manage_stock = 1 AND stock_quantity > $threshold))
        ");
        
        $low_stock = DB::get_var(DB::prepare("
            SELECT COUNT(*) FROM $table 
            WHERE status != 'trash' 
            AND manage_stock = 1 
            AND stock_quantity > 0 
            AND stock_quantity <= %d
        ", $threshold));
        
        $out_of_stock = DB::get_var("
            SELECT COUNT(*) FROM $table 
            WHERE status != 'trash' 
            AND (stock_status = 'outofstock' OR (manage_stock = 1 AND stock_quantity <= 0))
        ");
        
        wp_send_json_success([
            'total' => (int) $total,
            'in_stock' => (int) $in_stock,
            'low_stock' => (int) $low_stock,
            'out_of_stock' => (int) $out_of_stock
        ]);
    }
    
    /**
     * Save product from Impulse workspace
     */
    public function ajaxImpulseSaveProduct(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        
        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
        
        $data = [
            'name' => isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '',
            'sku' => isset($_POST['sku']) ? sanitize_text_field($_POST['sku']) : '',
            'price' => isset($_POST['price']) ? floatval($_POST['price']) : null,
            'sale_price' => isset($_POST['sale_price']) && $_POST['sale_price'] !== '' ? floatval($_POST['sale_price']) : null,
            'stock_quantity' => isset($_POST['stock']) ? intval($_POST['stock']) : null,
            'status' => isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'draft'
        ];
        
        // Handle featured image
        if (isset($_POST['featured_image'])) {
            $data['featured_image'] = $_POST['featured_image'] !== '' ? intval($_POST['featured_image']) : null;
        }
        
        // Handle short description
        if (isset($_POST['short_description'])) {
            $data['short_description'] = sanitize_textarea_field($_POST['short_description']);
        }
        
        // Handle long description
        if (isset($_POST['description'])) {
            $data['description'] = wp_kses_post($_POST['description']);
        }
        
        // Handle product type
        if (isset($_POST['product_type'])) {
            $allowed_types = ['physical', 'digital', 'subscription', 'variable', 'bundle'];
            $ptype = sanitize_text_field($_POST['product_type']);
            $data['product_type'] = in_array($ptype, $allowed_types) ? $ptype : 'physical';
        } elseif (isset($_POST['type'])) {
            // Legacy fallback
            $type = sanitize_text_field($_POST['type']);
            $data['product_type'] = $type === 'variable' ? 'variable' : 'physical';
        }
        
        // Handle attributes for variable products
        if (isset($_POST['attributes'])) {
            $data['attributes'] = sanitize_text_field($_POST['attributes']); // JSON string
        }
        
        // Handle gallery images (accept both gallery[] array and gallery_json string)
        $gallery = null;
        if (isset($_POST['gallery_json'])) {
            $gallery = json_decode(sanitize_textarea_field(wp_unslash($_POST['gallery_json'])), true);
            if (!is_array($gallery)) {
                $gallery = null;
            }
        } elseif (isset($_POST['gallery']) && is_array($_POST['gallery'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- array of IDs cast to int below
            $gallery = wp_unslash($_POST['gallery']);
        }

        if ($gallery !== null) {
            $gallery = array_map('intval', $gallery);
            $gallery = array_filter($gallery, function($id) { return $id > 0; });
            $gallery = array_values($gallery);
            $data['gallery_images'] = wp_json_encode($gallery);
            
            // Also populate images field with URLs for frontend use
            $imgTable = DB::prefix() . 'instinctor_images';
            $images = [];
            // Add featured image first
            $feat_id = $data['featured_image'] ?? null;
            if (!$feat_id && $id > 0) {
                // Load existing featured image from DB if not in this request
                $feat_id = DB::get_var(DB::prepare("SELECT featured_image FROM $table WHERE id = %d", $id));
            }
            if ($feat_id) {
                $url = $this->resolveImageUrl((int)$feat_id, $imgTable);
                if ($url) {
                    $images[] = ['id' => (int)$feat_id, 'url' => $url];
                }
            }
            // Add gallery images
            foreach ($gallery as $attId) {
                $url = $this->resolveImageUrl($attId, $imgTable);
                if ($url) {
                    $images[] = ['id' => $attId, 'url' => $url];
                }
            }
            $data['images'] = wp_json_encode($images);
        } elseif (!empty($data['featured_image'])) {
            // If no gallery but has featured image, populate images field
            $imgTable = DB::prefix() . 'instinctor_images';
            $url = $this->resolveImageUrl($data['featured_image'], $imgTable);
            if ($url) {
                $data['images'] = wp_json_encode([['id' => $data['featured_image'], 'url' => $url]]);
            }
        }
        
        // Handle meta (meta_title, meta_description, image_fit)
        $meta = [];
        if ($id > 0) {
            // Get existing meta
            $existing = DB::get_var(DB::prepare("SELECT meta FROM $table WHERE id = %d", $id));
            if ($existing) {
                $meta = json_decode($existing, true) ?: [];
            }
        }
        if (isset($_POST['meta_title'])) {
            $meta['meta_title'] = sanitize_text_field($_POST['meta_title']);
        }
        if (isset($_POST['meta_description'])) {
            $meta['meta_description'] = sanitize_textarea_field($_POST['meta_description']);
        }
        if (isset($_POST['image_fit'])) {
            $meta['image_fit'] = sanitize_text_field($_POST['image_fit']);
        }
        if (isset($_POST['variations'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- raw JSON; structurally validated on read
            $meta['variations'] = wp_unslash($_POST['variations']);
        }
        // Bundle fields → stored in meta + synced to instinctor_product_bundles
        if (!empty($_POST['bundle_type'])) {
            $meta['bundle_type']          = sanitize_text_field($_POST['bundle_type']);
            $meta['bundle_discount_type'] = sanitize_text_field($_POST['bundle_discount_type'] ?? 'percent');
            $meta['bundle_discount_value']= floatval($_POST['bundle_discount_value'] ?? 0);
            $meta['bundle_min']           = intval($_POST['bundle_min'] ?? 0);
            $meta['bundle_max']           = intval($_POST['bundle_max'] ?? 0);

            // Parse included product IDs
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON; structurally cast to ints below
            $bProducts = json_decode((string) wp_unslash($_POST['bundle_products'] ?? '[]'), true);
            if (!is_array($bProducts)) $bProducts = [];
            $bProducts = array_values(array_map('intval', $bProducts));
            $meta['bundle_products'] = wp_json_encode($bProducts);

            // Enrich with product names and images for display
            if (!empty($bProducts)) {
                $ptable = DB::prefix() . 'instinctor_products';
                $pids_placeholders = implode(',', array_fill(0, count($bProducts), '%d'));
                $prows = DB::get_results(
                    DB::prepare("SELECT id, name, images, featured_image FROM {$ptable} WHERE id IN ({$pids_placeholders})", ...$bProducts), // phpcs:ignore
                    ARRAY_A
                ) ?: [];
                $bNames  = [];
                $bImages = [];
                foreach ($prows as $pr) {
                    $bNames[$pr['id']] = $pr['name'];
                    $img = '';
                    if (!empty($pr['images'])) {
                        $imgs = json_decode($pr['images'], true);
                        $img = is_array($imgs) && !empty($imgs[0]['url']) ? $imgs[0]['url'] : '';
                    }
                    if (!$img && !empty($pr['featured_image'])) {
                        $src = wp_get_attachment_image_src(intval($pr['featured_image']), 'thumbnail');
                        $img = $src ? $src[0] : '';
                    }
                    if ($img) $bImages[$pr['id']] = $img;
                }
                $meta['bundle_product_names']  = $bNames;
                $meta['bundle_product_images'] = $bImages;
            } else {
                $meta['bundle_product_names']  = [];
                $meta['bundle_product_images'] = [];
            }
        }

        // Subscription fields → stored in meta
        if (isset($_POST['subscription_interval']) && $_POST['subscription_interval'] !== '') {
            $meta['subscription_interval'] = sanitize_text_field($_POST['subscription_interval']);
        }
        if (isset($_POST['subscription_interval_count']) && $_POST['subscription_interval_count'] !== '') {
            $meta['subscription_interval_count'] = max(1, (int) $_POST['subscription_interval_count']);
        }
        if (isset($_POST['trial_days']) && $_POST['trial_days'] !== '') {
            $meta['trial_days'] = max(0, (int) $_POST['trial_days']);
        }

        if (!empty($meta)) {
            $data['meta'] = wp_json_encode($meta);
        }
        
        // Generate slug if new product
        if (!$id && !empty($data['name'])) {
            $data['slug'] = sanitize_title($data['name']);
            // Make slug unique
            $base_slug = $data['slug'];
            $counter = 1;
            while (DB::get_var(DB::prepare("SELECT id FROM $table WHERE slug = %s", $data['slug']))) {
                $data['slug'] = $base_slug . '-' . $counter++;
            }
        }
        
        if ($id > 0) {
            DB::update($table, $data, ['id' => $id]);
        } else {
            if (!isset($data['product_type'])) {
                $data['product_type'] = 'physical';
            }
            $data['manage_stock'] = 1;
            DB::insert($table, $data);
            $id = DB::insert_id();
        }
        
        // Sync bundle product to instinctor_product_bundles table (for CartService compatibility)
        if (($data['product_type'] ?? '') === 'bundle' && !empty($meta['bundle_products'])) {
            $bundlesTable = DB::prefix() . 'instinctor_product_bundles';
            if (DB::get_var("SHOW TABLES LIKE '{$bundlesTable}'") === $bundlesTable) {
                $bSyncData = [
                    'name'           => $data['name'] ?? '',
                    'products'       => $meta['bundle_products'],
                    'bundle_type'    => $meta['bundle_type'] ?? 'fixed',
                    'discount_type'  => $meta['bundle_discount_type'] ?? 'percent',
                    'discount_value' => floatval($meta['bundle_discount_value'] ?? 0),
                    'min_products'   => intval($meta['bundle_min'] ?? 0),
                    'max_products'   => intval($meta['bundle_max'] ?? 0),
                    'is_active'      => ($data['status'] ?? 'draft') === 'published' ? 1 : 0,
                    'updated_at'     => current_time('mysql'),
                ];
                // Check if bundle already exists in legacy table by product id (store product_id in bundle table)
                $existingBundleId = DB::get_var(DB::prepare(
                    "SELECT id FROM {$bundlesTable} WHERE name = %s LIMIT 1",
                    $bSyncData['name']
                ));
                if ($existingBundleId) {
                    DB::update($bundlesTable, $bSyncData, ['id' => intval($existingBundleId)]);
                } else {
                    $bSyncData['created_at'] = current_time('mysql');
                    DB::insert($bundlesTable, $bSyncData);
                }
            }
        }

        // Handle categories
        if (isset($_POST['categories']) && is_array($_POST['categories'])) {
            $cat_table = DB::prefix() . 'instinctor_product_categories';
            // Remove existing
            DB::delete($cat_table, ['product_id' => $id]);
            // Add new
            foreach ($_POST['categories'] as $cat_id) {
                DB::insert($cat_table, [
                    'product_id' => $id,
                    'category_id' => intval($cat_id)
                ]);
            }
        }
        
        // Return updated product
        $product = DB::get_row(DB::prepare("SELECT * FROM $table WHERE id = %d", $id));
        
        // Get image URL
        $image_url = '';
        if ($product->featured_image) {
            $image_url = wp_get_attachment_image_url($product->featured_image, 'thumbnail');
        }
        
        wp_send_json_success([
            'id' => (int) $id,
            'product' => [
                'id' => (int) $product->id,
                'name' => $product->name,
                'slug' => $product->slug,
                'type' => $product->product_type,
                'subscription_interval' => '',
                'subscription_interval_count' => 1,
                'trial_days' => 0,
                'status' => $product->status,
                'price' => $product->price ? floatval($product->price) : null,
                'sale_price' => $product->sale_price ? floatval($product->sale_price) : null,
                'sku' => $product->sku ?: '',
                'stock' => $product->manage_stock ? (int) $product->stock_quantity : null,
                'stock_status' => $product->stock_status,
                'image' => $image_url
            ]
        ]);
    }
    
    /**
     * Delete product from Impulse workspace
     */
    public function ajaxImpulseDeleteProduct(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
        
        if (!$id) {
            wp_send_json_error(['message' => 'Invalid product ID']);
        }
        
        // Delete product categories
        DB::delete(DB::prefix() . 'instinctor_product_categories', ['product_id' => $id]);
        
        // Delete product
        DB::delete(DB::prefix() . 'instinctor_products', ['id' => $id]);
        
        wp_send_json_success(['deleted' => $id]);
    }
    
    /**
     * Bulk actions for products from Impulse workspace
     */
    public function ajaxImpulseBulkProducts(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        

        $p = DB::prefix();
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        
        $action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : '';
        $ids = isset($_POST['ids']) && is_array($_POST['ids']) ? array_map('intval', $_POST['ids']) : [];
        
        if (empty($ids)) {
            wp_send_json_error(['message' => 'No products selected']);
        }
        
        $idList = implode(',', $ids);
        
        switch ($action) {
            case 'publish':
                DB::query("UPDATE $table SET status = 'published' WHERE id IN ($idList)");
                break;
            case 'draft':
                DB::query("UPDATE $table SET status = 'draft' WHERE id IN ($idList)");
                break;
            case 'archive':
                DB::query("UPDATE $table SET status = 'archived' WHERE id IN ($idList)");
                break;
            case 'delete':
                DB::query("DELETE FROM {$p}instinctor_product_categories WHERE product_id IN ($idList)");
                DB::query("DELETE FROM $table WHERE id IN ($idList)");
                break;
            default:
                wp_send_json_error(['message' => 'Invalid action']);
        }
        
        wp_send_json_success(['affected' => count($ids)]);
    }
    
    /**
     * Reorder products from Impulse workspace
     */
    public function ajaxImpulseReorderProducts(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        
        $ids = isset($_POST['product_ids']) && is_array($_POST['product_ids']) ? array_map('intval', $_POST['product_ids']) : [];
        
        if (empty($ids)) {
            wp_send_json_error(['message' => 'No products to reorder']);
        }
        
        // Update sort_order for each product
        foreach ($ids as $order => $id) {
            DB::update($table, ['sort_order' => $order], ['id' => $id]);
        }
        
        wp_send_json_success(['reordered' => count($ids)]);
    }
    
    /**
     * AJAX: Update product status (Draft/Live)
     */
    public function ajaxImpulseUpdateProductStatus(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        $status = sanitize_text_field($_POST['status'] ?? '');
        
        if (!$id || !in_array($status, ['published', 'draft'])) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        DB::update(DB::prefix() . 'instinctor_products', ['status' => $status], ['id' => $id]);
        
        wp_send_json_success(['message' => 'Status updated']);
    }
    
    /**
     * AJAX: Update product featured status
     */
    public function ajaxImpulseUpdateProductFeatured(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        $featured = intval($_POST['featured'] ?? 0);
        
        if (!$id) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        DB::update(DB::prefix() . 'instinctor_products', ['is_featured' => $featured], ['id' => $id]);
        
        wp_send_json_success(['message' => 'Featured updated']);
    }
    
    /**
     * AJAX: Trash a product (soft delete)
     */
    public function ajaxImpulseTrashProduct(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        
        if (!$id) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        DB::update(DB::prefix() . 'instinctor_products', ['status' => 'trash'], ['id' => $id]);
        
        wp_send_json_success(['message' => 'Product trashed']);
    }
    
    /**
     * AJAX: Restore a trashed product
     */
    public function ajaxImpulseRestoreProduct(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        
        if (!$id) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        DB::update(DB::prefix() . 'instinctor_products', ['status' => 'draft'], ['id' => $id]);
        
        wp_send_json_success(['message' => 'Product restored']);
    }
    
    /**
     * AJAX: Duplicate a product
     */
    public function ajaxImpulseDuplicateProduct(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $id = intval($_POST['id'] ?? 0);
        
        if (!$id) {
            wp_send_json_error(['message' => 'Invalid request']);
        }
        
        $table = DB::prefix() . 'instinctor_products';
        
        // Get the original product
        $product = DB::get_row(DB::prepare("SELECT * FROM $table WHERE id = %d", $id), ARRAY_A);
        
        if (!$product) {
            wp_send_json_error(['message' => 'Product not found']);
        }
        
        // Remove the ID and modify the name
        unset($product['id']);
        $product['name'] = $product['name'] . ' (Copy)';
        $product['sku'] = $product['sku'] ? $product['sku'] . '-copy' : '';
        $product['status'] = 'draft';
        $product['created_at'] = current_time('mysql');
        $product['updated_at'] = current_time('mysql');
        
        // Insert the new product
        DB::insert($table, $product);
        $new_id = DB::insert_id();
        
        if ($new_id) {
            wp_send_json_success(['message' => 'Product duplicated', 'id' => $new_id]);
        } else {
            wp_send_json_error(['message' => 'Error duplicating product']);
        }
    }
    
    /**
     * AJAX: "Set as front page" action from the Page Grid right-click menu.
     * Sets the global `instinctor_front_page_id`; / serves it. Pages are
     * force-published since a draft can't be served as a front page.
     */
    public function ajaxImpulseSetFrontpage(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Unauthorized');
        }

        $pageId = (int) ($_POST['page_id'] ?? 0);
        if (!$pageId) {
            wp_send_json_error('Invalid page ID');
        }

        // Force-publish the page — a draft cannot be served as any front page.
        \Instinctor\Services\PageService::update($pageId, ['status' => 'published']);

        // Set the global site front page.
        \Instinctor\Core\Config::set('instinctor_front_page_id', $pageId);
        if (function_exists('update_option')) {
            if (function_exists('wp_update_post')) {
                wp_update_post(['ID' => $pageId, 'post_status' => 'publish']);
            }
            update_option('show_on_front', 'page');
            update_option('page_on_front', $pageId);
        }
        wp_send_json_success(['scope' => 'site']);
    }

    /**
     * AJAX: Save Impulse settings
     */
    public function ajaxImpulseSaveSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        // General settings
        if (!empty($_POST['store_name'])) {
            $store_name = sanitize_text_field($_POST['store_name']);
            Config::set('instinctor_store_name', $store_name);
            update_option('blogname', $store_name);
        }
        
        if (isset($_POST['store_description'])) {
            $store_description = sanitize_textarea_field($_POST['store_description']);
            Config::set('instinctor_store_description', $store_description);
            update_option('blogdescription', $store_description);
        }

        // Tagline — short one-liner used by the site footer and any other
        // "single sentence about the site" surface. Distinct from
        // store_description (which feeds Instagent + SEO meta and wants a
        // paragraph). Empty value is fine — the footer falls back to "©
        // {year} {site}. All rights reserved." with no em-dash clause.
        if (isset($_POST['store_tagline'])) {
            Config::set('instinctor_store_tagline', sanitize_text_field($_POST['store_tagline']));
        }

        if (isset($_POST['store_type']))  Config::set('instinctor_store_type',  sanitize_text_field($_POST['store_type']));
        // Salon mode: optional retail-products surface (off by default, booking-first).
        
        if (isset($_POST['store_email'])) Config::set('instinctor_store_email', sanitize_email($_POST['store_email']));
        if (isset($_POST['store_phone'])) Config::set('instinctor_store_phone', sanitize_text_field($_POST['store_phone']));
        if (isset($_POST['form_notify_sms'])) Config::set('instinctor_form_notify_sms', sanitize_text_field($_POST['form_notify_sms']));
        // Reviews: Google review link + the on-the-spot review-request SMS template
        // (used by the /admin/review-request tool — QR + text-the-link).
        if (isset($_POST['google_review_url']))      Config::set('instinctor_google_review_url',      esc_url_raw($_POST['google_review_url']));
        if (isset($_POST['review_request_message'])) Config::set('instinctor_review_request_message', sanitize_textarea_field($_POST['review_request_message']));

        // Brand profile — consumed by Instagent (AI Section Generator) as the
        // store-level context fed into Claude. Stored raw; Generator formats for
        // the prompt.
        if (isset($_POST['store_industry']))    Config::set('instinctor_store_industry',    sanitize_text_field($_POST['store_industry']));
        if (isset($_POST['store_audience']))    Config::set('instinctor_store_audience',    sanitize_textarea_field($_POST['store_audience']));
        if (isset($_POST['store_tone']))        Config::set('instinctor_store_tone',        sanitize_text_field($_POST['store_tone']));
        if (isset($_POST['store_value_prop']))     Config::set('instinctor_store_value_prop',     sanitize_textarea_field($_POST['store_value_prop']));
        if (isset($_POST['store_primary_cta']))    Config::set('instinctor_store_primary_cta',    sanitize_text_field($_POST['store_primary_cta']));
        if (isset($_POST['store_primary_color']))  Config::set('instinctor_store_primary_color',  sanitize_hex_color($_POST['store_primary_color']) ?: '#1d2327');
        if (isset($_POST['store_accent_color']))   Config::set('instinctor_store_accent_color',   sanitize_hex_color($_POST['store_accent_color']) ?: '#1d2327');
        if (isset($_POST['store_voice_sample']))   Config::set('instinctor_store_voice_sample',   sanitize_textarea_field($_POST['store_voice_sample']));
        if (isset($_POST['store_locale']))         Config::set('instinctor_store_locale',         sanitize_text_field($_POST['store_locale']));

        // Site logo - standalone-only field (WP installs use the Customizer's
        // Site Identity panel instead). The favicon has no field of its own:
        // it is generated from the logo here and stored in
        // instinctor_favicon_url for the existing consumers (MarketingShell,
        // impulse-page.php, AdminShell, /favicon.ico route). The panel
        // autosaves on every general-field change, so only regenerate when
        // the logo actually changed (or no favicon exists yet) - manually
        // set favicons from before the merge survive until then.
        if (isset($_POST['site_logo_url'])) {
            $newLogo = esc_url_raw($_POST['site_logo_url']);
            $oldLogo = (string) Config::get('instinctor_site_logo_url', '');
            Config::set('instinctor_site_logo_url', $newLogo);
            if ($newLogo === '') {
                $this->deleteGeneratedFavicon();
                Config::set('instinctor_favicon_url', '');
            } elseif ($newLogo !== $oldLogo || (string) Config::get('instinctor_favicon_url', '') === '') {
                $fav = $this->generateFaviconFromLogo($newLogo);
                if ($fav !== '') {
                    $this->deleteGeneratedFavicon();
                    Config::set('instinctor_favicon_url', $fav);
                }
            }
        }

        // Store options (tax, stock) — saved from general left column
        if (isset($_POST['tax_enabled']))              Config::set('instinctor_tax_enabled',              (bool)$_POST['tax_enabled']);
        if (isset($_POST['stock_management']))         Config::set('instinctor_stock_management',         (bool)$_POST['stock_management']);
        if (isset($_POST['low_stock_notifications']))  Config::set('instinctor_low_stock_notifications',  (bool)$_POST['low_stock_notifications']);

        // Digital Downloads global defaults
        if (isset($_POST['dd_default_limit']))  Config::set('instinctor_dd_default_limit',  intval($_POST['dd_default_limit']));
        if (isset($_POST['dd_default_expiry'])) Config::set('instinctor_dd_default_expiry', intval($_POST['dd_default_expiry']));
        if (isset($_POST['dd_email_subject']))  Config::set('instinctor_dd_email_subject',  sanitize_text_field($_POST['dd_email_subject']));
        
        // Store address
        if (isset($_POST['store_address_1'])) Config::set('instinctor_store_address_1', sanitize_text_field($_POST['store_address_1']));
        if (isset($_POST['store_address_2'])) Config::set('instinctor_store_address_2', sanitize_text_field($_POST['store_address_2']));
        if (isset($_POST['store_city']))      Config::set('instinctor_store_city',      sanitize_text_field($_POST['store_city']));
        if (isset($_POST['store_state']))     Config::set('instinctor_store_state',     sanitize_text_field($_POST['store_state']));
        if (isset($_POST['store_postcode']))  Config::set('instinctor_store_postcode',  sanitize_text_field($_POST['store_postcode']));
        if (isset($_POST['store_country']))   Config::set('instinctor_country',         sanitize_text_field($_POST['store_country']));
        if (!empty($_POST['currency_code'])) {
            Config::set('instinctor_currency', sanitize_text_field($_POST['currency_code']));
        }
        if (isset($_POST['currency_symbol'])) {
            Config::set('instinctor_currency_symbol', sanitize_text_field($_POST['currency_symbol']));
        }
        
        // Stripe settings — test/live mode is auto-detected from key prefix, no explicit flag needed
        if (!empty($_POST['stripe_publishable_key'])) {
            Config::set('instinctor_stripe_publishable_key', sanitize_text_field($_POST['stripe_publishable_key']));
        }
        if (!empty($_POST['stripe_secret_key'])) {
            Config::set('instinctor_stripe_secret_key', sanitize_text_field($_POST['stripe_secret_key']));
        }
        if (isset($_POST['stripe_webhook_secret'])) {
            Config::set('instinctor_stripe_webhook_secret', sanitize_text_field($_POST['stripe_webhook_secret']));
        }
        
        // PayPal settings — enabled when credentials present; sandbox flag from compact toggle
        if (isset($_POST['paypal_sandbox'])) {
            Config::set('instinctor_paypal_sandbox', (bool) $_POST['paypal_sandbox']);
        }
        if (!empty($_POST['paypal_client_id'])) {
            Config::set('instinctor_paypal_client_id', sanitize_text_field($_POST['paypal_client_id']));
        }
        if (!empty($_POST['paypal_secret'])) {
            Config::set('instinctor_paypal_secret', sanitize_text_field($_POST['paypal_secret']));
        }

        // Authorize.net settings
        if (isset($_POST['authorizenet_sandbox'])) {
            Config::set('instinctor_authorizenet_sandbox', (bool) $_POST['authorizenet_sandbox']);
        }
        if (!empty($_POST['authorizenet_api_login_id'])) {
            Config::set('instinctor_authorizenet_api_login_id', sanitize_text_field($_POST['authorizenet_api_login_id']));
        }
        if (!empty($_POST['authorizenet_transaction_key'])) {
            Config::set('instinctor_authorizenet_transaction_key', sanitize_text_field($_POST['authorizenet_transaction_key']));
        }
        if (!empty($_POST['authorizenet_public_client_key'])) {
            Config::set('instinctor_authorizenet_public_client_key', sanitize_text_field($_POST['authorizenet_public_client_key']));
        }

        // Google Sign-In credentials are owned by Platform now; nothing to save here.

        // Return policy
        if (isset($_POST['return_days']))     Config::set('instinctor_return_days',       max(1, intval($_POST['return_days'])));
        if (isset($_POST['restocking_fee']))  Config::set('instinctor_restocking_fee',    max(0, floatval($_POST['restocking_fee'])));
        if (isset($_POST['rma_notify_admin'])) Config::set('instinctor_rma_notify_admin', (bool)$_POST['rma_notify_admin']);
        if (isset($_POST['rma_auto_approve'])) Config::set('instinctor_rma_auto_approve', (bool)$_POST['rma_auto_approve']);
        
        // Checkout options
        if (isset($_POST['guest_checkout']))  Config::set('instinctor_guest_checkout',  (bool)$_POST['guest_checkout']);
        if (isset($_POST['create_account'])) Config::set('instinctor_create_account', (bool)$_POST['create_account']);
        if (isset($_POST['show_coupon']))    Config::set('instinctor_show_coupon',    (bool)$_POST['show_coupon']);
        if (isset($_POST['show_gift_card'])) Config::set('instinctor_show_gift_card', (bool)$_POST['show_gift_card']);
        if (isset($_POST['email_first']))    Config::set('instinctor_email_first',    (bool)$_POST['email_first']);
        
        wp_send_json_success(['message' => 'Settings saved']);
    }
    
    /**
     * AJAX: Save global code snippets (Code & Tracking section).
     * Receives full array as JSON body; replaces entire stored list.
     */
    /**
     * Build a favicon from the site logo. SVG logos are used directly (they
     * scale perfectly at favicon sizes). Raster logos are trimmed of
     * transparent padding and contained, centered, in a 128px transparent
     * PNG square stored under instinctor-brand/. The filename is
     * content-addressed so a new logo always yields a new URL, which
     * invalidates the browser's notoriously sticky favicon cache.
     *
     * Returns the favicon URL, or '' when nothing usable could be produced.
     * Logos that cannot be resolved to a local storage file (external URL,
     * missing GD) fall back to the logo URL itself - browsers accept any
     * image as a favicon, it is just not optimised.
     */
    private function generateFaviconFromLogo(string $logoUrl): string
    {
        $urlPath = (string) wp_parse_url($logoUrl, PHP_URL_PATH);
        if (strtolower(pathinfo($urlPath, PATHINFO_EXTENSION)) === 'svg') {
            return $logoUrl;
        }

        $rel      = '';
        $basePath = rtrim((string) wp_parse_url(\Instinctor\Core\Storage::baseurl(), PHP_URL_PATH), '/');
        if ($basePath !== '' && strpos($urlPath, $basePath . '/') === 0) {
            $rel = ltrim(substr($urlPath, strlen($basePath)), '/');
        }
        if ($rel === '' || !\Instinctor\Core\Storage::exists($rel) || !function_exists('imagecreatefromstring')) {
            return $logoUrl;
        }

        $raw = \Instinctor\Core\Storage::get($rel);
        $src = $raw !== false ? @imagecreatefromstring($raw) : false;
        if (!$src) {
            return $logoUrl;
        }
        imagepalettetotruecolor($src);

        // Downscale to a working copy first so the trim scan stays cheap.
        $w = imagesx($src);
        $h = imagesy($src);
        $scale = min(1.0, 256 / max($w, $h));
        $sw = max(1, (int) round($w * $scale));
        $sh = max(1, (int) round($h * $scale));
        $work = imagecreatetruecolor($sw, $sh);
        imagealphablending($work, false);
        imagesavealpha($work, true);
        imagefill($work, 0, 0, imagecolorallocatealpha($work, 0, 0, 0, 127));
        imagecopyresampled($work, $src, 0, 0, 0, 0, $sw, $sh, $w, $h);
        imagedestroy($src);

        // Bounding box of visibly opaque pixels (GD alpha: 0 opaque, 127
        // transparent). Opaque formats like JPEG report alpha 0 everywhere,
        // so the trim is a no-op for them.
        $minX = $sw; $minY = $sh; $maxX = -1; $maxY = -1;
        for ($y = 0; $y < $sh; $y++) {
            for ($x = 0; $x < $sw; $x++) {
                if (((imagecolorat($work, $x, $y) >> 24) & 0x7F) < 102) {
                    if ($x < $minX) $minX = $x;
                    if ($x > $maxX) $maxX = $x;
                    if ($y < $minY) $minY = $y;
                    if ($y > $maxY) $maxY = $y;
                }
            }
        }
        if ($maxX < 0) {
            imagedestroy($work);
            return $logoUrl;
        }
        $bw = $maxX - $minX + 1;
        $bh = $maxY - $minY + 1;

        $size = 128;
        $fit  = min($size / $bw, $size / $bh);
        $fw   = max(1, (int) round($bw * $fit));
        $fh   = max(1, (int) round($bh * $fit));
        $fav  = imagecreatetruecolor($size, $size);
        imagealphablending($fav, false);
        imagesavealpha($fav, true);
        imagefill($fav, 0, 0, imagecolorallocatealpha($fav, 0, 0, 0, 127));
        imagecopyresampled($fav, $work, (int) (($size - $fw) / 2), (int) (($size - $fh) / 2), $minX, $minY, $fw, $fh, $bw, $bh);
        imagedestroy($work);

        ob_start();
        imagepng($fav);
        $png = (string) ob_get_clean();
        imagedestroy($fav);
        if ($png === '') {
            return $logoUrl;
        }

        $favRel = 'instinctor-brand/favicon-' . substr(md5($png), 0, 10) . '.png';
        if (!\Instinctor\Core\Storage::put($favRel, $png)) {
            return $logoUrl;
        }
        return \Instinctor\Core\Storage::url($favRel);
    }

    /**
     * Delete the previously generated favicon file, if the current favicon
     * is one of ours (under instinctor-brand/). Manually set legacy
     * favicons and logo-as-favicon URLs are left alone.
     */
    private function deleteGeneratedFavicon(): void
    {
        $old = (string) Config::get('instinctor_favicon_url', '');
        if ($old === '') return;
        $urlPath = (string) wp_parse_url($old, PHP_URL_PATH);
        if (strpos($urlPath, 'instinctor-brand/favicon-') === false) return;
        $basePath = rtrim((string) wp_parse_url(\Instinctor\Core\Storage::baseurl(), PHP_URL_PATH), '/');
        if ($basePath !== '' && strpos($urlPath, $basePath . '/') === 0) {
            \Instinctor\Core\Storage::delete(ltrim(substr($urlPath, strlen($basePath)), '/'));
        }
    }

    

    /**
     * AJAX: Save store options (from Store workspace)
     */
    public function ajaxImpulseSaveStoreOptions(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        Config::set('instinctor_tax_enabled', !empty($_POST['tax_enabled']));
        Config::set('instinctor_stock_management', !empty($_POST['stock_management']));
        Config::set('instinctor_low_stock_threshold', intval($_POST['low_stock_threshold'] ?? 5));
        
        wp_send_json_success(['message' => 'Store options saved']);
    }
    
    /**
     * AJAX: Toggle email template enabled/disabled
     */
    public function ajaxImpulseToggleEmailTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $key = sanitize_text_field($_POST['key'] ?? '');
        $enabled = !empty($_POST['enabled']);
        
        if (empty($key)) {
            wp_send_json_error(['message' => 'No template specified']);
        }
        
        $templates = Config::get('instinctor_email_templates', []);
        if (!isset($templates[$key])) {
            $templates[$key] = [];
        }
        $templates[$key]['enabled'] = $enabled;
        Config::set('instinctor_email_templates', $templates);
        
        wp_send_json_success(['message' => 'Template updated']);
    }
    
    /**
     * AJAX: Get email template for editing
     */
    public function ajaxImpulseGetEmailTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $key = sanitize_text_field($_POST['key'] ?? '');
        
        if (empty($key)) {
            wp_send_json_error(['message' => 'No template specified']);
        }
        
        $template = \Instinctor\Services\EmailService::getTemplate($key);
        $defaults = \Instinctor\Services\EmailService::getDefaultTemplates();
        
        $data = [
            'key' => $key,
            'name' => $template['name'] ?? $defaults[$key]['name'] ?? ucwords(str_replace('_', ' ', $key)),
            'subject' => $template['subject'] ?? $defaults[$key]['subject'] ?? '',
            'heading' => $template['heading'] ?? $defaults[$key]['heading'] ?? '',
            'body' => $template['body'] ?? $defaults[$key]['body'] ?? '',
            'enabled' => $template['enabled'] ?? $defaults[$key]['enabled'] ?? false,
            'include_footer' => $template['include_footer'] ?? \Instinctor\Services\EmailService::getDefaultIncludeFooter($key),
        ];
        
        wp_send_json_success($data);
    }
    
    /**
     * AJAX: Save email template
     */
    public function ajaxImpulseSaveEmailTemplate(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $key = sanitize_text_field($_POST['key'] ?? '');
        
        if (empty($key)) {
            wp_send_json_error(['message' => 'No template specified']);
        }
        
        // Get existing template data to merge with
        $existing = \Instinctor\Services\EmailService::getTemplate($key);
        $defaults = \Instinctor\Services\EmailService::getDefaultTemplates();
        $defaultTemplate = $defaults[$key] ?? [];
        
        // Build save data, using existing values for any fields not provided.
        // WP.org B1 — sanitize subject/heading (plain text), body (HTML
        // template — wp_kses_post strips scripts/iframes/event handlers).
        $saveData = [
            'subject' => isset($_POST['subject']) ? sanitize_text_field(wp_unslash((string) $_POST['subject'])) : ($existing['subject'] ?? $defaultTemplate['subject'] ?? ''),
            'heading' => isset($_POST['heading']) ? sanitize_text_field(wp_unslash((string) $_POST['heading'])) : ($existing['heading'] ?? $defaultTemplate['heading'] ?? ''),
            'body'    => isset($_POST['body'])    ? wp_kses_post(wp_unslash((string) $_POST['body']))         : ($existing['body']    ?? $defaultTemplate['body']    ?? ''),
            'enabled' => $existing['enabled'] ?? $defaultTemplate['enabled'] ?? true, // Preserve enabled state
            'include_footer' => isset($_POST['include_footer']) ? !empty($_POST['include_footer']) : ($existing['include_footer'] ?? true),
        ];
        
        $result = \Instinctor\Services\EmailService::saveTemplate($key, $saveData);
        
        if ($result) {
            wp_send_json_success(['message' => 'Template saved']);
        } else {
            wp_send_json_error(['message' => 'Failed to save template']);
        }
    }
    
    /**
     * AJAX: Send test email
     */
    public function ajaxImpulseSendTestEmail(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $key = sanitize_text_field($_POST['key'] ?? '');
        $adminEmail = Config::get('admin_email');
        
        if (empty($key)) {
            wp_send_json_error(['message' => 'No template specified']);
        }
        
        // Build test data
        $testData = [
            'customer_name' => 'Test Customer',
            'customer_email' => $adminEmail,
            'order_number' => '12345',
            'order_total' => Config::get('instinctor_currency_symbol', '$') . '99.99',
            'tracking_number' => 'TEST123456789',
            'tracking_url' => home_url(),
            'store_name' => get_bloginfo('name'),
            'rma_number' => 'RMA-0001',
            'points' => '500',
            'tier' => 'Gold',
            'new_tier' => 'Platinum',
        ];
        
        // Get template content from request (for preview of unsaved changes)
        $subject = sanitize_text_field($_POST['subject'] ?? '') ?: 'Test Email';
        $heading = sanitize_text_field($_POST['heading'] ?? '') ?: 'Test Email';
        $body = wp_kses_post(sanitize_text_field(wp_unslash($_POST['body'] ?? ''))) ?: 'This is a test email.';
        
        // Replace placeholders
        foreach ($testData as $placeholder => $value) {
            $subject = str_replace('{' . $placeholder . '}', $value, $subject);
            $heading = str_replace('{' . $placeholder . '}', $value, $heading);
            $body = str_replace('{' . $placeholder . '}', $value, $body);
        }
        
        // Build HTML email using the real template system
        $htmlEmail = \Instinctor\Services\EmailService::buildTestEmail($heading, $body);
        
        // Send via EmailService relay (same path as real transactional emails)
        $result = \Instinctor\Services\EmailService::sendDirectEmail($adminEmail, '[TEST] ' . $subject, $htmlEmail);
        
        if ($result) {
            wp_send_json_success(['message' => 'Test email sent', 'email' => $adminEmail]);
        } else {
            wp_send_json_error(['message' => 'Failed to send test email. Check your email settings.']);
        }
    }
    
    /**
     * AJAX: Get email footer settings
     */
    public function ajaxImpulseGetEmailFooterSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $settings = \Instinctor\Services\EmailService::getFooterSettings();
        wp_send_json_success($settings);
    }
    
    /**
     * AJAX: Save email footer settings
     */
    public function ajaxImpulseSaveEmailFooterSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        // Merge submitted fields with existing settings — preserves extended fields
        // (show_city_state_zip, store_name_size/weight, etc.) not present in the form
        $existing = \Instinctor\Services\EmailService::getFooterSettings();

        $settings = array_merge($existing, [
            'show_logo'       => !empty($_POST['show_logo']),
            'show_store_name' => !empty($_POST['show_store_name']),
            'show_address'    => !empty($_POST['show_address']),
            'show_email'      => !empty($_POST['show_email']),
            'show_phone'      => !empty($_POST['show_phone']),
            'text_align'      => sanitize_text_field($_POST['text_align'] ?? 'center'),
            'custom_text'     => sanitize_textarea_field($_POST['custom_text'] ?? ''),
        ]);

        // update_option returns false when value is unchanged — treat that as success
        \Instinctor\Services\EmailService::saveFooterSettings($settings);
        wp_send_json_success(['message' => 'Footer settings saved']);
    }
    
    /**
     * AJAX: Activate an add-on
     */
    public function ajaxImpulseActivateAddon(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $addon = sanitize_text_field($_POST['addon'] ?? '');
        
        if (empty($addon)) {
            wp_send_json_error(['message' => 'No add-on specified']);
        }
        
        // Get current active addons
        $activeAddons = Config::get('instinctor_active_addons', []);
        
        // Dev mode bypass - either constant in wp-config.php or option from License Manager
        $dev_mode = \Instinctor\Core\TierManager::isDevMode();
        
        // Tier gating removed — all addons available
        
        // Add to active addons if not already
        if (!in_array($addon, $activeAddons)) {
            $activeAddons[] = $addon;
            Config::set('instinctor_active_addons', $activeAddons);
        }
        
        wp_send_json_success(['message' => 'Add-on activated', 'addon' => $addon]);
    }
    
    public function ajaxImpulseDeactivateAddon(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $addon = sanitize_text_field($_POST['addon'] ?? '');
        
        if (empty($addon)) {
            wp_send_json_error(['message' => 'No add-on specified']);
        }
        
        // Get current active addons
        $activeAddons = Config::get('instinctor_active_addons', []);
        
        // Remove from active addons
        $activeAddons = array_filter($activeAddons, function($a) use ($addon) {
            return $a !== $addon;
        });
        
        Config::set('instinctor_active_addons', array_values($activeAddons));
        
        wp_send_json_success(['message' => 'Add-on deactivated', 'addon' => $addon]);
    }
    
    /**
     * Activate/validate license key against ILM server
     */
    public function ajaxImpulseActivateLicense(): void
    {
        // WordPress.org distribution prohibits remote license validation.
        // Pro is a separate plugin downloaded from instinctor.com — the user
        // needs to install the Pro zip via Plugins -> Add New -> Upload Plugin.
        // This message gives them the direct path to that download.
        wp_send_json_error([
            'message'  => 'Pro features require a separate plugin. Download the Pro version at https://instinctor.com/upgrade, then upload it via Plugins -> Add New -> Upload Plugin to replace this free version.',
            'cta_url'  => 'https://instinctor.com/upgrade',
            'cta_text' => 'Get Pro',
        ]);
    }
    /**
     * Proxy checkout request to ILM server (avoids CORS issues)
     */
    public function ajaxImpulseProxyCheckout(): void
    {
        // WordPress.org build can't run in-plugin checkout; send the
        // 'Upgrade to Pro' button to the external purchase page (the admin
        // JS reads checkout_url and redirects there). The customer buys,
        // downloads the Pro plugin zip, and gets a license key.
        wp_send_json_success(['checkout_url' => 'https://instinctor.com/upgrade']);
    }
    /**
     * Toggle addon enabled/disabled state
     */
    public function ajaxToggleAddon(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $addon = sanitize_text_field($_POST['addon'] ?? '');
        $enable = !empty($_POST['enable']);
        
        if (empty($addon)) {
            wp_send_json_error(['message' => 'No add-on specified']);
        }
        
        // Get current active addons
        $activeAddons = Config::get('instinctor_active_addons', []);
        
        // Business-tier addons list
        $proAddons = [
            'ai_content', 'live_chat',
            'bookings', 'subscriptions', 'software_licensing', 'preorders',
            'vendors', 'gift_cards', 'loyalty', 'dropshipping'
        ];
        
        $dev_mode = \Instinctor\Core\TierManager::isDevMode();
        
        if ($enable) {
            // Tier gating removed — all addons available
            
            // Add to active addons if not already
            if (!in_array($addon, $activeAddons)) {
                $activeAddons[] = $addon;
                Config::set('instinctor_active_addons', $activeAddons);
            }
            
            wp_send_json_success([
                'message' => 'Add-on enabled',
                'addon' => $addon,
                'reload' => true // Hint to JS to reload page to update UI
            ]);
        } else {
            // Remove from active addons
            $activeAddons = array_filter($activeAddons, function($a) use ($addon) {
                return $a !== $addon;
            });
            
            Config::set('instinctor_active_addons', array_values($activeAddons));
            
            wp_send_json_success([
                'message' => 'Add-on disabled',
                'addon' => $addon,
                'reload' => true
            ]);
        }
    }
    
    /**
     * AJAX: Save AfterShip settings
     */
    public function ajaxSaveAfterShip(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $result = \Instinctor\Services\AfterShipService::saveSettings([
            'enabled' => !empty($_POST['enabled']),
            'api_key' => sanitize_text_field($_POST['api_key'] ?? ''),
            'update_frequency' => sanitize_text_field($_POST['update_frequency'] ?? 'hourly'),
            'auto_detect_carrier' => true,
        ]);
        
        if ($result) {
            wp_send_json_success([
                'message' => 'AfterShip settings saved',
                'settings' => \Instinctor\Services\AfterShipService::getSettings()
            ]);
        } else {
            wp_send_json_error(['message' => 'Failed to save settings']);
        }
    }
    
    /**
     * AJAX: Test AfterShip connection
     */
    public function ajaxTestAfterShip(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        // Temporarily set the API key for testing
        $apiKey = sanitize_text_field($_POST['api_key'] ?? '');
        
        if (empty($apiKey)) {
            wp_send_json_error(['message' => 'API key is required']);
        }
        
        // Save temporarily for the test
        $currentSettings = \Instinctor\Services\AfterShipService::getSettings();
        \Instinctor\Services\AfterShipService::saveSettings([
            'enabled' => true,
            'api_key' => $apiKey,
            'update_frequency' => $currentSettings['update_frequency'] ?? 'hourly',
        ]);
        
        $result = \Instinctor\Services\AfterShipService::testConnection();
        
        // Restore original settings if test was just for validation
        if (!$currentSettings['enabled']) {
            \Instinctor\Services\AfterShipService::saveSettings($currentSettings);
        }
        
        if ($result['success']) {
            wp_send_json_success($result);
        } else {
            wp_send_json_error($result);
        }
    }
    
    /**
     * Save workspace preferences (order + default) per user
     */
    public function ajaxSaveWorkspacePrefs(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('edit_posts')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $user_id = Auth::userId();
        $valid_workspaces = ['chat', 'store', 'impulse'];

        // Save order
        if (isset($_POST['order'])) {
            $order = array_map('sanitize_text_field', (array) $_POST['order']);
            $order = array_values(array_intersect($order, $valid_workspaces));
            if (count($order) === 3) {
                if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
                    update_user_meta($user_id, 'instinctor_workspace_order', $order);
                } else {
                    Config::set('instinctor_workspace_order_' . $user_id, $order);
                }
            }
        }

        // Save store section default
        $valid_store_sections = ['orders', 'products', 'bookings', 'settings'];
        if (isset($_POST['default_store_section'])) {
            $val = sanitize_text_field($_POST['default_store_section']);
            if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
                if (in_array($val, $valid_store_sections)) update_user_meta($user_id, 'instinctor_store_section_default', $val);
                else delete_user_meta($user_id, 'instinctor_store_section_default');
            } else {
                if (in_array($val, $valid_store_sections)) Config::set('instinctor_store_section_default_' . $user_id, $val);
                else Config::delete('instinctor_store_section_default_' . $user_id);
            }
        }

        // Save SEO tab default
        $valid_seo_tabs = ['pages', 'settings'];
        if (isset($_POST['default_seo_tab'])) {
            $val = sanitize_text_field($_POST['default_seo_tab']);
            if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
                if (in_array($val, $valid_seo_tabs)) update_user_meta($user_id, 'instinctor_seo_tab_default', $val);
                else delete_user_meta($user_id, 'instinctor_seo_tab_default');
            } else {
                if (in_array($val, $valid_seo_tabs)) Config::set('instinctor_seo_tab_default_' . $user_id, $val);
                else Config::delete('instinctor_seo_tab_default_' . $user_id);
            }
        }

        // Save device default
        $valid_devices = ['desktop', 'tablet', 'mobile'];
        if (isset($_POST['default_device'])) {
            $val = sanitize_text_field($_POST['default_device']);
            if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
                if (in_array($val, $valid_devices)) update_user_meta($user_id, 'instinctor_device_default', $val);
                else delete_user_meta($user_id, 'instinctor_device_default');
            } else {
                if (in_array($val, $valid_devices)) Config::set('instinctor_device_default_' . $user_id, $val);
                else Config::delete('instinctor_device_default_' . $user_id);
            }
        }
        
        wp_send_json_success();
    }
    
    /**
     * Dismiss an upsell prompt
     */
    public function ajaxDismissUpsell(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }
        
        $key = sanitize_text_field($_POST['key'] ?? '');
        if (empty($key)) {
            wp_send_json_error(['message' => 'No key specified']);
        }
        
        $dismissed = Config::get('instinctor_upsell_dismissed', []);
        $dismissed[$key] = current_time('timestamp');
        Config::set('instinctor_upsell_dismissed', $dismissed);
        
        wp_send_json_success();
    }

    /**
     * Get team members with Instinctor roles/capabilities.
     */
    public function ajaxGetTeam(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $rm        = \Instinctor\Core\RoleManager::instance();
        $users     = $rm->getTeamMembers(); // dual-mode: WP or standalone
        $roles     = $rm->getAssignableRoles();
        $available = [];

        // WP mode only: populate the "add existing WP user" dropdown
        $isWP = !defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress';
        if ($isWP && function_exists('get_users')) {
            $all_users = get_users(['fields' => ['ID', 'display_name', 'user_email']]);
            foreach ($all_users as $u) {
                $available[] = [
                    'id'    => $u->ID,
                    'name'  => $u->display_name,
                    'email' => $u->user_email,
                ];
            }
        }

        wp_send_json_success([
            'team'      => $users,
            'available' => $available,
            'roles'     => $roles,
        ]);
    }

    /**
     * Set a user's Instinctor role.
     */
    public function ajaxSetUserRole(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied']);
        }

        $user_id = intval($_POST['user_id'] ?? 0);
        $role = sanitize_text_field($_POST['role'] ?? '');
        
        if (!$user_id) {
            wp_send_json_error(['message' => 'Invalid user']);
        }

        $rm = \Instinctor\Core\RoleManager::instance();
        $result = $rm->setTeamMemberRole($user_id, $role); // dual-mode
        
        if (!$result) {
            wp_send_json_error(['message' => 'Cannot modify administrator roles']);
        }

        wp_send_json_success(['message' => 'Role updated']);
    }

    // =========================================================================
    // DIGITAL DOWNLOAD HANDLERS
    // =========================================================================

    public function ajaxGetProductDownloads(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $productId = (int) ($_POST['product_id'] ?? 0);
        if (!$productId) { wp_send_json_error('Missing product_id'); return; }
        $service = \Instinctor\Services\DigitalDownloadService::instance();
        wp_send_json_success(['downloads' => $service->getProductDownloads($productId)]);
    }

    public function ajaxSaveProductDownload(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $productId = (int) ($_POST['product_id'] ?? 0);
        if (!$productId) { wp_send_json_error('Missing product_id'); return; }
        $service = \Instinctor\Services\DigitalDownloadService::instance();
        $id = $service->saveDownload($productId, $_POST);
        if ($id === false) { wp_send_json_error('Failed to save download'); return; }
        wp_send_json_success(['id' => $id, 'downloads' => $service->getProductDownloads($productId)]);
    }

    public function ajaxDeleteProductDownload(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $downloadId = (int) ($_POST['download_id'] ?? 0);
        $productId  = (int) ($_POST['product_id'] ?? 0);
        if (!$downloadId || !$productId) { wp_send_json_error('Missing params'); return; }
        $service = \Instinctor\Services\DigitalDownloadService::instance();
        $service->deleteDownload($downloadId, $productId);
        wp_send_json_success(['downloads' => $service->getProductDownloads($productId)]);
    }

    public function ajaxToggleProductDownloadable(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $productId    = (int) ($_POST['product_id'] ?? 0);
        $downloadable = (sanitize_text_field(wp_unslash($_POST['downloadable'] ?? '0'))) === '1' ? 1 : 0;
        if (!$productId) { wp_send_json_error('Missing product_id'); return; }
        DB::update(DB::prefix() . 'instinctor_products', ['downloadable' => $downloadable], ['id' => $productId]);
        wp_send_json_success(['downloadable' => $downloadable]);
    }

    public function ajaxGetCustomerDownloads(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $customerId = (int) ($_POST['customer_id'] ?? 0);
        if (!$customerId) { wp_send_json_error('Missing customer_id'); return; }
        $service = \Instinctor\Services\DigitalDownloadService::instance();
        wp_send_json_success(['downloads' => $service->getCustomerDownloads($customerId)]);
    }

    public function ajaxGetDownloadStats(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $service = \Instinctor\Services\DigitalDownloadService::instance();
        wp_send_json_success($service->getDownloadStats());
    }

    public function ajaxSaveDDSettings(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }
        $limit   = isset($_POST['default_limit']) && $_POST['default_limit'] !== '' ? max(0, (int)$_POST['default_limit']) : '';
        $expiry  = isset($_POST['default_expiry']) && $_POST['default_expiry'] !== '' ? max(0, (int)$_POST['default_expiry']) : '';
        $subject = sanitize_text_field($_POST['email_subject'] ?? 'Your download is ready');
        Config::set('instinctor_dd_default_limit',   $limit);
        Config::set('instinctor_dd_default_expiry',  $expiry);
        Config::set('instinctor_dd_email_subject',   $subject);
        wp_send_json_success(['message' => 'Settings saved']);
    }

    // =========================================================================
    // BILLING DASHBOARD HANDLERS (M4)
    // =========================================================================

    public function ajaxGetBillingData(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');

        $p = DB::prefix();
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }

        $table = DB::prefix() . 'instinctor_licenses';

        // Licenses with customer email joined
        $licenses = DB::get_results(
            "SELECT l.id, l.license_key, l.addon, l.status, l.activation_limit, l.expires_at, l.created_at,
                    c.email AS customer_email,
                    (SELECT COUNT(*) FROM {$p}instinctor_license_activations a WHERE a.license_id = l.id) AS activation_count
             FROM {$table} l
             LEFT JOIN {$p}instinctor_customers c ON c.id = l.customer_id
             ORDER BY l.created_at DESC
             LIMIT 200",
            ARRAY_A
        ) ?: [];

        $activeCount = 0;
        foreach ($licenses as $l) {
            if ($l['status'] === 'active') $activeCount++;
        }

        // MRR: sum from active subscriptions
        $mrr = (float) DB::get_var(
            "SELECT SUM(CASE WHEN period = 'year' THEN price/12 ELSE price END)
             FROM {$p}instinctor_subscriptions
             WHERE status = 'active'"
        );

        wp_send_json_success([
            'licenses'     => $licenses,
            'active_count' => $activeCount,
            'mrr'          => number_format($mrr, 2),
        ]);
    }

    public function ajaxSaveStripePlans(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }

        $allowed = ['builder_monthly', 'builder_annual', 'business_monthly', 'business_annual'];
        $plans   = (array) ($_POST['plans'] ?? []);

        foreach ($allowed as $key) {
            if (isset($plans[$key])) {
                Config::set('instinctor_stripe_plan_' . $key, sanitize_text_field($plans[$key]));
            }
        }

        wp_send_json_success(['message' => 'Plans saved']);
    }

    public function ajaxSaveBillingUrls(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) { wp_send_json_error('Permission denied'); return; }

        $allowed = ['instinctor_pricing_page_url', 'instinctor_account_page_url', 'instinctor_support_url'];
        $urls    = (array) ($_POST['urls'] ?? []);

        foreach ($allowed as $key) {
            if (isset($urls[$key])) {
                update_option($key, esc_url_raw($urls[$key]));
            }
        }

        wp_send_json_success(['message' => 'URLs saved']);
    }

    // =========================================================================
    // PRODUCT BUNDLE HANDLERS
    // =========================================================================

    public function ajaxGetBundles(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $bundles = \Instinctor\Services\ProductBundleService::getBundles();

        // Enrich with product names for display
        $products_table = DB::prefix() . 'instinctor_products';
        foreach ($bundles as &$bundle) {
            $bundle['products'] = json_decode($bundle['products'] ?? '[]', true) ?: [];
            if (!empty($bundle['products'])) {
                $ids = array_map('intval', $bundle['products']);
                $placeholders = implode(',', array_fill(0, count($ids), '%d'));
                $rows = DB::get_results(
                    DB::prepare("SELECT id, name, images, featured_image FROM {$products_table} WHERE id IN ({$placeholders})", ...$ids), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    ARRAY_A
                ) ?: [];
                $bundle['product_names']  = array_column($rows, 'name', 'id');
                $bundle['product_images'] = [];
                foreach ($rows as $row) {
                    $img = '';
                    if (!empty($row['images'])) {
                        $imgs = json_decode($row['images'], true);
                        $img = is_array($imgs) && !empty($imgs[0]['url']) ? $imgs[0]['url'] : (is_array($imgs) && !empty($imgs[0]) && is_string($imgs[0]) ? $imgs[0] : '');
                    }
                    if (!$img && !empty($row['featured_image'])) {
                        $src = wp_get_attachment_image_src(intval($row['featured_image']), 'thumbnail');
                        $img = $src ? $src[0] : '';
                    }
                    if ($img) $bundle['product_images'][$row['id']] = $img;
                }
            } else {
                $bundle['product_names']  = [];
                $bundle['product_images'] = [];
            }
        }

        wp_send_json_success(['bundles' => $bundles]);
    }

    public function ajaxSaveBundle(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $data = [
            'id'             => intval($_POST['id'] ?? 0),
            'name'           => sanitize_text_field($_POST['name'] ?? ''),
            'description'    => sanitize_textarea_field($_POST['description'] ?? ''),
            'bundle_type'    => sanitize_text_field($_POST['bundle_type'] ?? 'fixed'),
            'discount_type'  => sanitize_text_field($_POST['discount_type'] ?? 'percent'),
            'discount_value' => floatval($_POST['discount_value'] ?? 0),
            'min_products'   => intval($_POST['min_products'] ?? 0),
            'max_products'   => intval($_POST['max_products'] ?? 0),
            'is_active'      => intval($_POST['is_active'] ?? 1),
            'products'       => array_map('intval', (array) ($_POST['products'] ?? [])),
        ];

        if (empty($data['name'])) {
            wp_send_json_error('Bundle name is required');
        }

        $id = \Instinctor\Services\ProductBundleService::saveBundle($data);
        if (!$id) {
            wp_send_json_error('Failed to save bundle');
        }

        wp_send_json_success(['id' => $id, 'message' => 'Bundle saved']);
    }

    public function ajaxDeleteBundle(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $id = intval($_POST['id'] ?? 0);
        if (!$id) {
            wp_send_json_error('Invalid bundle ID');
        }

        if (\Instinctor\Services\ProductBundleService::deleteBundle($id)) {
            wp_send_json_success(['message' => 'Bundle deleted']);
        } else {
            wp_send_json_error('Failed to delete bundle');
        }
    }

    // =========================================================================
    // PRODUCT ADD-ON HANDLERS
    // =========================================================================

    public function ajaxGetProductAddons(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $productId = intval($_GET['product_id'] ?? 0);

        if ($productId) {
            $addons = \Instinctor\Services\ProductAddonService::getAllAddons($productId);
        } else {
            // All global add-ons for the management list
            $table  = DB::prefix() . 'instinctor_product_addons';
            $addons = DB::get_results(
                "SELECT * FROM {$table} ORDER BY product_id IS NULL DESC, sort_order ASC",
                ARRAY_A
            ) ?: [];
            foreach ($addons as &$addon) {
                $addon['options'] = json_decode($addon['options'] ?? '[]', true) ?: [];
            }
        }

        wp_send_json_success(['addons' => $addons]);
    }

    public function ajaxSaveProductAddon(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $data = [
            'id'          => intval($_POST['id'] ?? 0),
            'product_id'  => !empty($_POST['product_id']) ? intval($_POST['product_id']) : null,
            'name'        => sanitize_text_field($_POST['name'] ?? ''),
            'description' => sanitize_textarea_field($_POST['description'] ?? ''),
            'type'        => sanitize_text_field($_POST['type'] ?? 'checkbox'),
            'price'       => floatval($_POST['price'] ?? 0),
            'price_type'  => sanitize_text_field($_POST['price_type'] ?? 'flat'),
            'options'     => sanitize_textarea_field($_POST['options'] ?? ''),
            'required'    => intval($_POST['required'] ?? 0),
            'max_length'  => intval($_POST['max_length'] ?? 0),
            'is_active'   => intval($_POST['is_active'] ?? 1),
            'sort_order'  => intval($_POST['sort_order'] ?? 0),
        ];

        if (empty($data['name'])) {
            wp_send_json_error('Add-on name is required');
        }

        $id = \Instinctor\Services\ProductAddonService::saveAddon($data);
        if (!$id) {
            wp_send_json_error('Failed to save add-on');
        }

        wp_send_json_success(['id' => $id, 'message' => 'Add-on saved']);
    }

    public function ajaxDeleteProductAddon(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $id = intval($_POST['id'] ?? 0);
        if (!$id) {
            wp_send_json_error('Invalid add-on ID');
        }

        if (\Instinctor\Services\ProductAddonService::deleteAddon($id)) {
            wp_send_json_success(['message' => 'Add-on deleted']);
        } else {
            wp_send_json_error('Failed to delete add-on');
        }
    }

    // =========================================================================
    // VOLUME DISCOUNT HANDLERS
    // =========================================================================

    public function ajaxGetVolumeTiers(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $productId  = !empty($_REQUEST['product_id'])  ? intval($_REQUEST['product_id'])  : null;
        $categoryId = !empty($_REQUEST['category_id']) ? intval($_REQUEST['category_id']) : null;

        if ($productId) {
            $tiers = \Instinctor\Services\VolumeDiscountService::getProductTiers($productId);
        } elseif ($categoryId) {
            $tiers = \Instinctor\Services\VolumeDiscountService::getCategoryTiers($categoryId);
        } else {
            // All tiers for management view
            $table = DB::prefix() . 'instinctor_volume_discounts';
            $tiers = DB::get_results(
                "SELECT * FROM {$table} ORDER BY product_id IS NOT NULL DESC, min_quantity ASC",
                ARRAY_A
            ) ?: [];
        }

        wp_send_json_success(['tiers' => $tiers]);
    }

    public function ajaxSaveVolumeTier(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $data = [
            'id'             => intval($_POST['id'] ?? 0),
            'product_id'     => !empty($_POST['product_id'])  ? intval($_POST['product_id'])  : null,
            'category_id'    => !empty($_POST['category_id']) ? intval($_POST['category_id']) : null,
            'min_quantity'   => intval($_POST['min_quantity'] ?? 2),
            'max_quantity'   => !empty($_POST['max_quantity']) ? intval($_POST['max_quantity']) : null,
            'discount_type'  => sanitize_text_field($_POST['discount_type'] ?? 'percent'),
            'discount_value' => floatval($_POST['discount_value'] ?? 0),
        ];

        if ($data['min_quantity'] < 2) {
            wp_send_json_error('Minimum quantity must be 2 or more');
        }
        if ($data['discount_value'] <= 0) {
            wp_send_json_error('Discount value must be greater than 0');
        }

        $id = \Instinctor\Services\VolumeDiscountService::saveTier($data);
        if (!$id) {
            wp_send_json_error('Failed to save tier');
        }

        wp_send_json_success(['id' => $id, 'message' => 'Tier saved']);
    }

    public function ajaxDeleteVolumeTier(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $id = intval($_POST['id'] ?? 0);
        if (!$id) {
            wp_send_json_error('Invalid tier ID');
        }

        if (\Instinctor\Services\VolumeDiscountService::deleteTier($id)) {
            wp_send_json_success(['message' => 'Tier deleted']);
        } else {
            wp_send_json_error('Failed to delete tier');
        }
    }

    // =========================================================================
    // ABANDONED CART SETTINGS
    // =========================================================================

    // =========================================================================
    // LOCALIZATION: LANGUAGE MANAGEMENT
    // =========================================================================


    // =========================================================================
    // LOCALIZATION: CURRENCY MANAGEMENT
    // =========================================================================

    public function ajaxGetCurrencies(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $currencies    = \Instinctor\Services\CurrencyService::getEnabledCurrencies();
        $allCodes      = \Instinctor\Services\CurrencyService::getAvailableCurrencyCodes();
        $defaultCode   = '';
        $default       = \Instinctor\Services\CurrencyService::getDefaultCurrency();
        if ($default) {
            $defaultCode = $default->code;
        }

        wp_send_json_success([
            'currencies'  => $currencies,
            'available'   => array_keys($allCodes),
            'default'     => $defaultCode,
        ]);
    }

    /**
     * Show upgrade notice on WordPress admin pages (not on Instinctor page)
     */
    public function showUpgradeNotice(): void
    {
        // Only for admins
        if (!Auth::can('manage_options')) return;
        
        // Don't show on the Instinctor page (it has its own upsell UI)
        $screen = get_current_screen();
        if ($screen && strpos($screen->id, 'instinctor') !== false) return;
        
        // Only show if free tier
        $tier = Config::get('instinctor_license_tier', 'free');
        return; // PRO badges/upsell removed
    }

    // ── T10 — API Credentials Panel (ILM absorption) ─────────────────────────

    /**
     * AJAX: Get API credentials for the credentials settings panel.
     * Returns obfuscated values — secrets shown as masked strings.
     */
    public function ajaxGetCredentials(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied.']);
        }

        // Helper: mask a secret key — show first 4 + last 4, rest as *
        $mask = static function (string $val): string {
            if (strlen($val) <= 10) {
                return $val ? str_repeat('*', strlen($val)) : '';
            }
            return substr($val, 0, 4) . str_repeat('*', strlen($val) - 8) . substr($val, -4);
        };

        wp_send_json_success([
            // Email sender identity — FROM name/address only.
            // Delivery goes through Instinctor Dashboard relay (platform.instinctor.com).
            // API keys never live here.
            'email' => [
                'from_email' => Config::get('store_from_email', ''),
                'from_name'  => Config::get('store_from_name', ''),
            ],
            // SMS/WhatsApp/AI relay through platform — no credentials stored on store side.
            // Store owner enables the features; platform handles delivery.
            'stripe' => [
                'publishable_key' => Config::get('instinctor_stripe_publishable_key', ''),
                'secret_key'      => $mask(Config::get('instinctor_stripe_secret_key', '')),
                'webhook_secret'  => $mask(Config::get('instinctor_stripe_webhook_secret', '')),
                'mode'            => Config::get('instinctor_stripe_mode', 'test'),
            ],
            'license' => [
                'key'         => $mask(Config::get('instinctor_license_key', '')),
                'tier'        => Config::get('instinctor_license_tier', 'free'),
                'email'       => Config::get('instinctor_registered_email', ''),
                'site_mode'   => \Instinctor\Core\Plugin::siteMode(),
                'site_status' => Config::get('instinctor_site_status', null),
            ],
        ]);
    }

    /**
     * AJAX: Save API credentials.
     * Empty string means "no change" — existing secret preserved.
     * Explicitly passing a value overwrites it.
     */
    public function ajaxSaveCredentials(): void
    {
        check_ajax_referer('instinctor_admin', 'nonce');
        if (!Auth::can('manage_options')) {
            wp_send_json_error(['message' => 'Permission denied.']);
        }

        $group = sanitize_text_field(wp_unslash($_POST['group'] ?? ''));
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- structured array; validated as_array + key/value sanitize below
        $data  = isset($_POST['data']) ? wp_unslash($_POST['data']) : [];

        if (!is_array($data)) {
            wp_send_json_error(['message' => 'Invalid data.']);
        }

        // Only save non-empty values (empty = "no change" for secrets)
        $save = static function (string $key, mixed $value, bool $isSecret = false): void {
            if ($isSecret && empty($value)) {
                return; // blank field = keep existing
            }
            Config::set($key, sanitize_text_field((string) $value));
        };

        switch ($group) {
            case 'email':
                // Only sender identity — no API keys
                $save('store_from_email', $data['from_email'] ?? '');
                $save('store_from_name',  $data['from_name']  ?? '');
                break;

            case 'stripe':
                $save('instinctor_stripe_publishable_key', $data['publishable_key'] ?? '');
                $save('instinctor_stripe_secret_key',      $data['secret_key']      ?? '', true);
                $save('instinctor_stripe_webhook_secret',  $data['webhook_secret']  ?? '', true);
                $save('instinctor_stripe_mode',            $data['mode']            ?? 'test');
                break;

            default:
                wp_send_json_error(['message' => 'Unknown credentials group: ' . $group]);
        }

        wp_send_json_success(['message' => ucfirst($group) . ' credentials saved.']);
    }

    /**
     * Register an AJAX handler for both WordPress and standalone modes.
     * On WordPress: add_action('wp_ajax_' . $action)
     * On standalone: AjaxDispatcher::register($action)
     */
    private function registerAjax(string $action, callable $handler): void
    {
        if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
            add_action('wp_ajax_' . $action, $handler);
        } else {
            \Instinctor\Core\AjaxDispatcher::register($action, $handler);
        }
    }

    /**
     * Resolve an image ID to a URL — checks instinctor_images first, falls back to WP.
     */
    private function resolveImageUrl(int $id, string $imgTable): string
    {
        if ($id <= 0) return '';
        // Try instinctor_images table (standalone-compatible)
        $row = DB::get_row(
            DB::prepare("SELECT rel_path FROM {$imgTable} WHERE id = %d AND parent_id IS NULL LIMIT 1", $id),
            'ARRAY_A'
        );
        if ($row && !empty($row['rel_path'])) {
            $url = Storage::url($row['rel_path']);
            if ($url) return $url;
        }
        // Fallback to WP media (only in WP mode)
        if (function_exists('wp_get_attachment_image_url')) {
            $url = wp_get_attachment_image_url($id, 'large');
            if ($url) return $url;
        }
        return '';
    }

}
