<?php
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.SafeRedirect.wp_redirect_wp_redirect, WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText, WordPress.DateTime.RestrictedFunctions.date_date, WordPress.WP.AlternativeFunctions.rand_mt_rand, WordPress.PHP.DevelopmentFunctions.error_log_error_log, Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_Admin {
    
    private $square_api;

    private function csv_cell($value) {
        if ($value === null) {
            return '';
        }

        $value = (string) $value;
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
        if ($value !== '' && preg_match('/^[=\+\-@]/', ltrim($value))) {
            $value = "'" . $value;
        }

        return $value;
    }

    private function csv_row($values) {
        return array_map(array($this, 'csv_cell'), $values);
    }
    
    public function __construct() {
        $this->square_api = new YATOON_Square_API();
        
        add_action('admin_menu', array($this, 'add_admin_menu'));
        add_action('admin_post_yatoon_wizard_save',    array($this, 'handle_wizard_save'));
        add_action('admin_post_yatoon_dismiss_wizard', array($this, 'handle_wizard_dismiss'));
        add_action('admin_post_yatoon_start_test_booking', array($this, 'handle_start_test_booking'));
        add_action('admin_notices', array($this, 'show_wizard_notice'));
        add_action('admin_init', array($this, 'register_settings'));
        // Redirect back to plugin page after options.php saves yatoon_settings
        add_action('admin_init', array($this, 'redirect_after_settings_save'));
        add_action('admin_notices', array($this, 'maybe_render_setup_notice'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
        add_action('admin_post_yatoon_save_business_hours', array($this, 'handle_save_business_hours'));
        add_action('admin_post_yatoon_save_ai_settings', array($this, 'handle_save_ai_settings'));
        add_action('wp_ajax_yatoon_test_connection', array($this, 'ajax_test_connection'));
        add_action('wp_ajax_yatoon_test_vagaro_connection', array($this, 'ajax_test_vagaro_connection'));
        add_action('wp_ajax_yatoon_sync_now', array($this, 'ajax_sync_now'));
        add_action('wp_ajax_yatoon_delete_service', array($this, 'ajax_delete_service'));
        add_action('wp_ajax_yatoon_get_staff_services', array($this, 'ajax_get_staff_services'));
        add_action('wp_ajax_yatoon_update_staff_services', array($this, 'ajax_update_staff_services'));
        add_action('wp_ajax_yatoon_update_option', array($this, 'ajax_update_option'));
        add_action('wp_ajax_yatoon_save_general_settings', array($this, 'ajax_save_general_settings'));
        add_action('wp_ajax_yatoon_create_staff_portal_page', array($this, 'ajax_create_staff_portal_page'));
        add_action('wp_ajax_yatoon_add_option',    array($this, 'ajax_add_option'));
        add_action('wp_ajax_yatoon_delete_option', array($this, 'ajax_delete_option'));
        add_action('wp_ajax_yatoon_get_service_options', array($this, 'ajax_get_service_options'));
        add_action('wp_ajax_yatoon_reorder_options', array($this, 'ajax_reorder_options'));
        add_action('wp_ajax_yatoon_get_addon_options', array($this, 'ajax_get_addon_options'));
        add_action('wp_ajax_yatoon_delete_bookings', array($this, 'ajax_delete_bookings'));
        add_action('wp_ajax_yatoon_delete_single_booking', array($this, 'ajax_delete_single_booking'));
        add_action('wp_ajax_yatoon_export_bookings', array($this, 'ajax_export_bookings'));
        add_action('wp_ajax_yatoon_delete_waitlist', array($this, 'ajax_delete_waitlist'));
        add_action('wp_ajax_yatoon_export_waitlist', array($this, 'ajax_export_waitlist'));
        add_action('wp_ajax_yatoon_export_reports', array($this, 'ajax_export_reports'));
        add_action('wp_ajax_yatoon_check_waitlist_now', array($this, 'ajax_check_waitlist_now'));
        add_action('wp_ajax_yatoon_notify_waitlist_customer', array($this, 'ajax_notify_waitlist_customer'));
        add_action('wp_ajax_yatoon_delete_messages', array($this, 'ajax_delete_messages'));
        add_action('wp_ajax_yatoon_export_messages', array($this, 'ajax_export_messages'));
        add_action('wp_ajax_yatoon_get_service', array($this, 'ajax_get_service'));
        add_action('wp_ajax_yatoon_save_service', array($this, 'ajax_save_service'));
        add_action('wp_ajax_yatoon_rename_service_category', array($this, 'ajax_rename_service_category'));
        add_action('wp_ajax_yatoon_delete_service_category', array($this, 'ajax_delete_service_category'));
        add_action('wp_ajax_yatoon_save_service_images', array($this, 'ajax_save_service_images'));
        add_action('wp_ajax_yatoon_get_service_images', array($this, 'ajax_get_service_images'));
        add_action('wp_ajax_yatoon_save_form_field',    array($this, 'ajax_save_form_field'));
        add_action('wp_ajax_yatoon_delete_form_field',  array($this, 'ajax_delete_form_field'));
        add_action('wp_ajax_yatoon_toggle_form_field',  array($this, 'ajax_toggle_form_field'));
        add_action('wp_ajax_yatoon_reorder_form_fields',array($this, 'ajax_reorder_form_fields'));
        add_action('wp_ajax_yatoon_save_package',       array($this, 'ajax_save_package'));
        add_action('wp_ajax_yatoon_delete_package',     array($this, 'ajax_delete_package'));
        add_action('wp_ajax_yatoon_toggle_package',     array($this, 'ajax_toggle_package'));
        add_action('wp_ajax_yatoon_create_gift_card',        array($this, 'ajax_create_gift_card'));
        add_action('wp_ajax_yatoon_void_gift_card',          array($this, 'ajax_void_gift_card'));
        add_action('wp_ajax_yatoon_get_gift_card_history',   array($this, 'ajax_get_gift_card_history'));
        add_action('wp_ajax_yatoon_validate_gift_card',      array($this, 'ajax_validate_gift_card'));
        add_action('wp_ajax_nopriv_yatoon_validate_gift_card', array($this, 'ajax_validate_gift_card'));
        add_action('wp_ajax_yatoon_toggle_service', array($this, 'ajax_toggle_service'));
        add_action('wp_ajax_yatoon_update_booking_status', array($this, 'ajax_update_booking_status'));
        add_action('wp_ajax_yatoon_update_payment_status', array($this, 'ajax_update_payment_status'));
        add_action('wp_ajax_yatoon_get_admin_reschedule_slots', array($this, 'ajax_get_admin_reschedule_slots'));
        add_action('wp_ajax_yatoon_admin_reschedule_booking', array($this, 'ajax_admin_reschedule_booking'));
        add_action('wp_ajax_yatoon_save_calendar_staff_filter', array($this, 'ajax_save_calendar_staff_filter'));
        add_action('wp_ajax_yatoon_save_calendar_timezone',     array($this, 'ajax_save_calendar_timezone'));
        add_action('wp_ajax_yatoon_send_payment_request', array($this, 'ajax_send_payment_request'));
        add_action('wp_ajax_yatoon_send_recall_email', array($this, 'ajax_send_recall_email'));
        add_action('wp_ajax_yatoon_twilio_test', array($this, 'ajax_twilio_test'));
        add_action('wp_ajax_yatoon_save_client_notes', array($this, 'ajax_save_client_notes'));
        add_action('wp_ajax_yatoon_toggle_addon_setting', array($this, 'ajax_toggle_addon_setting'));
        add_action('wp_ajax_yatoon_get_client',        array($this, 'ajax_get_client'));

        // Admin-post.php handlers for each settings tab
        add_action( 'admin_post_yatoon_save_general_settings',  array( $this, 'handle_save_general' ) );
        add_action( 'admin_post_yatoon_save_email_settings',    array( $this, 'handle_save_email' ) );
        add_action( 'admin_post_yatoon_save_sms_settings',      array( $this, 'handle_save_sms' ) );
        add_action( 'admin_post_yatoon_save_calendar_settings', array( $this, 'handle_save_calendar' ) );
        add_action( 'admin_post_yatoon_save_brand_settings',    array( $this, 'handle_save_brand' ) );

        // Reschedule cron when sync interval changes
        add_action('update_option_yatoon_sync_interval', array($this, 'reschedule_cron'), 10, 2);
        add_action('update_option_yatoon_auto_sync_enabled', array($this, 'reschedule_cron_on_toggle'), 10, 2);

        // Reschedule booking-only sync when its settings change
        add_action('update_option_yatoon_booking_sync_enabled',  array( 'YATOON_Cron', 'maybe_reschedule_booking_sync' ) );
        add_action('update_option_yatoon_booking_sync_interval', array( 'YATOON_Cron', 'maybe_reschedule_booking_sync' ) );
    }

    public function maybe_render_setup_notice() {
        if (!current_user_can('manage_options')) {
            return;
        }

        if ( ! class_exists( 'YATOON_App_Shell' ) || ! YATOON_App_Shell::is_booking_admin_page() ) {
            return;
        }

        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $service_count = (int) yatoon_db_get_var("SELECT COUNT(*) FROM {$wpdb->prefix}yatoon_services");
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $staff_count = (int) yatoon_db_get_var("SELECT COUNT(*) FROM {$wpdb->prefix}yatoon_staff");
        $page_id = (int) get_option('yatoon_manage_booking_page_id', 0);
        $book_url = '';
        $booking_page = get_page_by_path('booking');
        if ($booking_page) {
            $book_url = get_permalink($booking_page->ID);
        } else {
            $booking_query = new WP_Query( array(
                'post_type'              => 'page',
                'title'                  => 'Booking',
                'post_status'            => 'publish',
                'posts_per_page'         => 1,
                'no_found_rows'          => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false,
            ) );
            if ( $booking_query->have_posts() ) {
                $book_url = get_permalink( $booking_query->posts[0]->ID );
            }
            wp_reset_postdata();
        }

        if ($service_count === 0 || $staff_count === 0) {
            echo '<div class="notice notice-warning"><p><strong>Yatoon Booking is almost ready.</strong> Add at least one service and one staff member to start accepting bookings.</p><p><a class="button button-primary" href="' . esc_url(admin_url('admin.php?page=sbs-services')) . '">Set up services</a> <a class="button" href="' . esc_url(admin_url('admin.php?page=sbs-staff')) . '">Set up staff</a></p></div>';
            return;
        }

        if (get_option('yatoon_starter_data_seeded', '') === '1') {
            echo '<div class="notice notice-success is-dismissible"><p><strong>Starter booking data is live.</strong> We added a demo team and services so you can test the flow immediately.</p><p>';
            if ($book_url) {
                echo '<a class="button button-primary" href="' . esc_url($book_url) . '" target="_blank" rel="noreferrer">Open booking page</a> ';
            }
            echo '<a class="button" href="' . esc_url(admin_url('admin.php?page=sbs-services')) . '">Edit services</a>';
            if ($page_id > 0) {
                echo ' <a class="button" href="' . esc_url(get_permalink($page_id)) . '" target="_blank" rel="noreferrer">Open manage booking page</a>';
            }
            echo '</p></div>';
            update_option('yatoon_starter_data_seeded', 'dismissed');
        }
    }

    /**
     * Reschedule cron job when interval changes
     */
    public function reschedule_cron($old_value, $new_value) {
        // Only reschedule if auto sync is actually enabled
        if ( ! get_option('yatoon_auto_sync_enabled', '1') ) return;
        $valid = array('5min','15min','hourly','daily','weekly','monthly');
        if ( ! in_array($new_value, $valid, true) ) return;
        $timestamp = wp_next_scheduled('yatoon_sync_square_data');
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'yatoon_sync_square_data');
        }
        wp_schedule_event(time(), $new_value, 'yatoon_sync_square_data');
    }

    /**
     * Schedule or unschedule cron when auto sync toggle changes
     */
    public function reschedule_cron_on_toggle($old_value, $new_value) {
        $timestamp = wp_next_scheduled('yatoon_sync_square_data');
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'yatoon_sync_square_data');
        }
        if ($new_value === '1') {
            $interval = get_option('yatoon_sync_interval', 'hourly');
            wp_schedule_event(time(), $interval, 'yatoon_sync_square_data');
        }
    }
    
    /**
     * Add admin menu
     * PRO menu items - show Pro: in Free version
     */
    public function add_admin_menu() {
        $is_pro = function_exists('ybs_is_pro') ? ybs_is_pro() : false;
        $lock = $is_pro ? '' : ' Pro:';
        $show_advanced_menu = get_option('yatoon_show_advanced_menu', '0') === '1';
        $advanced_parent = $show_advanced_menu ? 'yatoon-booking-system' : null;

        add_menu_page(
            'Yatoon Booking',
            'Yatoon Booking',
            'manage_options',
            'yatoon-booking-system',
            array($this, 'display_settings_page'),
            'dashicons-calendar-alt',
            30
        );

        // -- Daily operations first, then setup, growth, and advanced tools.
        add_submenu_page( 'yatoon-booking-system', 'Launch Center', 'Launch Center', 'manage_options', 'yatoon-dashboard', array($this, 'display_dashboard_page') );
        add_submenu_page( 'yatoon-booking-system', 'Bookings', 'Bookings', 'manage_options', 'sbs-bookings', array($this, 'display_bookings_page') );
        add_submenu_page( 'yatoon-booking-system', 'Calendar', 'Calendar', 'manage_options', 'yatoon-calendar', array($this, 'display_calendar_page') );
        add_submenu_page( 'yatoon-booking-system', 'Clients', 'Clients', 'manage_options', 'yatoon-clients', array($this, 'display_clients_page') );
        add_submenu_page( 'yatoon-booking-system', 'Services', 'Services', 'manage_options', 'sbs-services', array($this, 'display_services_page') );
        add_submenu_page( 'yatoon-booking-system', 'Staff', 'Staff', 'manage_options', 'sbs-staff', array($this, 'display_staff_page') );
        add_submenu_page( 'yatoon-booking-system', 'Reports', 'Reports', 'manage_options', 'yatoon-reports', array($this, 'display_reports_page') );
        add_submenu_page( 'yatoon-booking-system', 'AI Assistant', 'AI Assistant', 'manage_options', 'yatoon-ai-assistant', array($this, 'display_ai_assistant_page') );
        add_submenu_page( 'yatoon-booking-system', 'Add-ons', 'Add-ons', 'manage_options', 'yatoon-addons', array($this, 'display_addons_page') );
        add_submenu_page( 'yatoon-booking-system', 'Brand & Colors', 'Brand & Colors' . $lock, 'manage_options', 'sbs-brand', array($this, 'display_brand_page') );
        add_submenu_page( 'yatoon-booking-system', 'Settings', 'Settings', 'manage_options', 'yatoon-booking-system', array($this, 'display_settings_page') );

        // -- Advanced/growth tools can be shown from Settings when needed.
        add_submenu_page( $advanced_parent, 'Messages', 'Messages', 'manage_options', 'sbs-messages', array($this, 'display_messages_page') );
        add_submenu_page( $advanced_parent, 'Waitlist', 'Waitlist', 'manage_options', 'sbs-waitlist', array($this, 'display_waitlist_page') );
        add_submenu_page( $advanced_parent, 'Catalog & Gallery', 'Catalog & Gallery', 'manage_options', 'yatoon-catalog-settings', array($this, 'display_catalog_settings_page') );
        add_submenu_page( $advanced_parent, 'Service Menu', 'Service Menu', 'manage_options', 'yatoon-service-menu', array($this, 'display_service_menu_page') );
        add_submenu_page( $advanced_parent, 'Gift Cards', 'Gift Cards', 'manage_options', 'yatoon-gift-cards', array($this, 'display_gift_cards_page') );
        add_submenu_page( $advanced_parent, 'Service Packages', 'Packages' . $lock, 'manage_options', 'yatoon-packages', array($this, 'display_packages_page') );
        add_submenu_page( $advanced_parent, 'Reviews', 'Reviews *', 'manage_options', 'yatoon-reviews', array($this, 'display_reviews_page') );
        add_submenu_page( $advanced_parent, 'Recurring', 'Recurring', 'manage_options', 'yatoon-recurring', array($this, 'display_recurring_page') );
        add_submenu_page( $advanced_parent, 'Closed Dates', 'Closed Dates', 'manage_options', 'yatoon-closed-dates', array($this, 'display_closed_dates_page') );
        add_submenu_page( $advanced_parent, 'Form Fields', 'Form Fields', 'manage_options', 'yatoon-form-fields', array($this, 'display_form_fields_page') );
        add_submenu_page( $advanced_parent, 'Setup Wizard', 'Setup Wizard', 'manage_options', 'yatoon-setup-wizard', array($this, 'display_wizard_page') );
        add_submenu_page( $advanced_parent, 'Notifications', 'Notifications', 'manage_options', 'sbs-notifications', array($this, 'display_notifications_page') );
        add_submenu_page( $advanced_parent, 'Sync Log', 'Sync Log', 'manage_options', 'sbs-sync-log', array($this, 'display_sync_log_page') );
        add_submenu_page( $advanced_parent, 'Blacklist', 'Blacklist', 'manage_options', 'sbs-blacklist', array($this, 'display_blacklist_page') );

        // -- PRO menu (shown with Pro: in Free) -------------
        add_submenu_page( $advanced_parent, 'SMS Notifications',   'SMS Notifications'   . $lock, 'manage_options', 'sbs-sms-pro',     array($this, 'display_sms_pro_page') );
        add_submenu_page( $advanced_parent, 'Google Calendar',     'Google Calendar'     . $lock, 'manage_options', 'sbs-gcal-pro',    array($this, 'display_gcal_pro_page') );
        add_submenu_page( $advanced_parent, 'Deposit & Prepay',    'Deposit & Prepay'    . $lock, 'manage_options', 'yatoon-deposit-pro',   array($this, 'display_deposit_pro_page') );
        add_submenu_page( $advanced_parent, 'No-show Tracking',   'No-show Tracking'    . $lock, 'manage_options', 'yatoon-noshow-pro',    array($this, 'display_noshow_pro_page') );
        add_submenu_page( $advanced_parent, 'Waitlist Auto-notify','Waitlist Auto-notify'. $lock, 'manage_options', 'yatoon-waitlist-pro',  array($this, 'display_waitlist_pro_page') );
    }
    
    /**
     * Register settings
     */
    /**
     * Redirect back to our settings page after options.php save
     */
    public function redirect_after_settings_save() {
        // Hook wp_redirect as early as possible so we catch options.php's redirect
        if ( ! empty( $_POST['option_page'] ) && 'yatoon_settings' === sanitize_key( wp_unslash( $_POST['option_page'] ) ) ) {
            $tab = ! empty( $_POST['yatoon_return_tab'] ) ? sanitize_key( wp_unslash( $_POST['yatoon_return_tab'] ) ) : 'general';
            add_filter( 'wp_redirect', function( $location ) use ( $tab ) {
                if ( strpos( $location, 'settings-updated' ) !== false || strpos( $location, 'options.php' ) !== false ) {
                    return admin_url( 'admin.php?page=yatoon-booking-system&tab=' . $tab . '&settings-updated=true' );
                }
                return $location;
            }, 1 );
        }
    }


    // -- Admin-post.php save handlers -----------------------------------------

    private function save_options( $keys, $nonce_action, $checkboxes = array() ) {
        check_admin_referer( $nonce_action );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Forbidden' );

        $protected_empty_keys = array(
            'yatoon_square_access_token',
            'yatoon_vagaro_api_key',
            'yatoon_deposit_stripe_secret',
            'yatoon_stripe_webhook_secret',
            'yatoon_square_webhook_signature_key',
            'yatoon_twilio_auth_token',
            'yatoon_google_cal_client_secret',
            'yatoon_google_cal_refresh_token',
        );

        foreach ( $keys as $key ) {
            if ( isset( $_POST[ $key ] ) ) {
                $raw = wp_unslash( $_POST[ $key ] );
                $val = ( substr( $key, -6 ) === '_color' || substr( $key, -7 ) === '_colour' )
                    ? sanitize_hex_color( $raw )
                    : $this->sanitize_setting( $raw );
                if ( $key === 'yatoon_staff_pin_length' ) {
                    $val = in_array( (int) $val, array( 4, 6 ), true ) ? (string) (int) $val : '4';
                }
                if ( $val === '' && in_array( $key, $protected_empty_keys, true ) ) {
                    continue;
                }
                if ( YATOON_Crypto::is_sensitive( $key ) ) {
                    YATOON_Crypto::update_option( $key, $val );
                } else {
                    update_option( $key, $val );
                }
            } elseif ( in_array( $key, $checkboxes, true ) ) {
                update_option( $key, '0' );
            }
        }

        $tab = isset( $_POST['yatoon_return_tab'] ) ? sanitize_key( wp_unslash( $_POST['yatoon_return_tab'] ) ) : 'general';
        wp_safe_redirect( add_query_arg( array(
            'page' => 'yatoon-booking-system',
            'tab'  => $tab,
            'saved' => '1',
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    public function handle_save_general() {
        $this->save_options( array(
            'yatoon_active_platform', 'yatoon_booking_template', 'yatoon_square_environment', 'sbs_square_environment',
            'yatoon_square_access_token', 'yatoon_square_location_id',
            'yatoon_vagaro_api_key', 'yatoon_vagaro_business_id',
            'yatoon_auto_sync_enabled', 'yatoon_sync_interval',
            'yatoon_booking_sync_enabled', 'yatoon_booking_sync_interval',
            'yatoon_square_webhook_signature_key',
            'yatoon_staff_idle_lock_minutes',
            'yatoon_staff_pin_length',
            'yatoon_phone_mode', 'yatoon_phone_required', 'yatoon_phone_preset_country',
            'yatoon_load_external_phone_assets', 'yatoon_use_ip_geo_phone_detect',
            'yatoon_verify_email_otp', 'yatoon_verify_sms_otp', 'yatoon_otp_expiry_minutes',
            'yatoon_policy_enabled', 'yatoon_policy_require_ack',
            'yatoon_policy_title', 'yatoon_policy_text', 'yatoon_policy_ack_label',
            'yatoon_consent_enabled', 'yatoon_consent_title', 'yatoon_consent_text',
            'yatoon_consent_signature_label',
            'yatoon_waitlist_enabled', 'yatoon_waitlist_auto_notify',
            'yatoon_multi_appointment', 'yatoon_max_guests_together',
            'yatoon_multi_location_enabled', 'yatoon_booking_immersive_mode',
            'yatoon_show_next_opening', 'yatoon_show_availability_source_note',
            'yatoon_allow_design_upload',
            'yatoon_availability_help_text', 'yatoon_latest_end_hour',
            'yatoon_show_success_next', 'yatoon_success_next_title',
            'yatoon_success_action_note', 'yatoon_success_followup_text',
            'yatoon_success_deposit_text', 'yatoon_success_pay_later_text',
            'yatoon_rebooking_nudge_text', 'yatoon_rebooking_nudge_btn',
            'yatoon_show_venue_hero', 'yatoon_venue_kicker',
            'yatoon_venue_deposit_label', 'yatoon_venue_pay_later_label',
            'yatoon_tip_enabled', 'yatoon_tip_label', 'yatoon_tip_presets',
            'yatoon_gift_card_enabled', 'yatoon_enable_coupons',
            'yatoon_show_portal_banner', 'yatoon_portal_banner_bg',
            'yatoon_portal_banner_text', 'yatoon_portal_banner_prompt',
            'yatoon_portal_url', 'yatoon_portal_label', 'yatoon_portal_new_tab',
            'yatoon_portal_login_title', 'yatoon_portal_login_subtitle',
            'yatoon_portal_login_brand', 'yatoon_portal_btn_label',
            'yatoon_portal_primary_color', 'yatoon_book_new_url',
            'yatoon_show_ratings_on_cards', 'yatoon_public_rating_label',
            'yatoon_deposit_enabled', 'yatoon_deposit_amount', 'yatoon_deposit_type',
            'yatoon_deposit_stripe_key', 'yatoon_deposit_stripe_secret',
            'yatoon_stripe_webhook_secret', 'yatoon_payment_request_url',
            'yatoon_square_checkout_url', 'yatoon_square_app_url',
            'yatoon_square_checkout_note',
            'yatoon_whatsapp_enabled', 'yatoon_whatsapp_number',
            'yatoon_whatsapp_prefill', 'yatoon_whatsapp_label', 'yatoon_text_us_enabled',
            'yatoon_ai_service_helper_enabled', 'yatoon_ai_reply_drafts_enabled',
            'yatoon_textus_bg_color', 'yatoon_textus_text_color',
            'yatoon_pwa_enabled', 'yatoon_contact_phone', 'yatoon_contact_email',
            'yatoon_business_address', 'yatoon_show_advanced_menu', 'yatoon_test_booking_mode',
            'yatoon_banner_bg_color',
            'yatoon_banner_btn_color', 'yatoon_banner_text_color',
        ), 'yatoon_save_general_settings', array(
            'yatoon_auto_sync_enabled', 'yatoon_booking_sync_enabled',
            'yatoon_phone_required', 'yatoon_verify_email_otp', 'yatoon_verify_sms_otp',
            'yatoon_load_external_phone_assets', 'yatoon_use_ip_geo_phone_detect',
            'yatoon_policy_enabled', 'yatoon_policy_require_ack', 'yatoon_consent_enabled',
            'yatoon_waitlist_enabled', 'yatoon_waitlist_auto_notify',
            'yatoon_multi_appointment', 'yatoon_multi_location_enabled',
            'yatoon_booking_immersive_mode', 'yatoon_show_next_opening',
            'yatoon_allow_design_upload',
            'yatoon_show_availability_source_note', 'yatoon_show_success_next',
            'yatoon_show_venue_hero', 'yatoon_tip_enabled', 'yatoon_gift_card_enabled',
            'yatoon_enable_coupons', 'yatoon_show_portal_banner', 'yatoon_portal_new_tab',
            'yatoon_show_ratings_on_cards', 'yatoon_deposit_enabled',
            'yatoon_whatsapp_enabled', 'yatoon_text_us_enabled', 'yatoon_ai_service_helper_enabled',
            'yatoon_ai_reply_drafts_enabled', 'yatoon_pwa_enabled',
            'yatoon_show_advanced_menu', 'yatoon_test_booking_mode',
        ) );
        if ( class_exists( 'YATOON_Cron' ) ) YATOON_Cron::maybe_reschedule_booking_sync();
    }

    public function handle_save_ai_settings() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Forbidden' );
        }
        check_admin_referer( 'yatoon_save_ai_settings' );

        update_option( 'yatoon_ai_service_helper_enabled', ! empty( $_POST['yatoon_ai_service_helper_enabled'] ) ? '1' : '0' );
        update_option( 'yatoon_ai_reply_drafts_enabled', ! empty( $_POST['yatoon_ai_reply_drafts_enabled'] ) ? '1' : '0' );
        $frontend_title = isset( $_POST['yatoon_ai_frontend_title'] ) ? sanitize_text_field( wp_unslash( $_POST['yatoon_ai_frontend_title'] ) ) : '';
        update_option( 'yatoon_ai_frontend_title', $frontend_title !== '' ? $frontend_title : __( 'AI Style Assistant', 'yatoon-booking-system' ) );
        $privacy_note = isset( $_POST['yatoon_ai_privacy_note'] ) ? sanitize_text_field( wp_unslash( $_POST['yatoon_ai_privacy_note'] ) ) : '';
        update_option( 'yatoon_ai_privacy_note', $privacy_note !== '' ? $privacy_note : __( 'Privacy note: Please avoid sensitive, medical, or payment details.', 'yatoon-booking-system' ) );
        $frontend_font_size = isset( $_POST['yatoon_ai_frontend_font_size'] ) ? absint( wp_unslash( $_POST['yatoon_ai_frontend_font_size'] ) ) : 14;
        update_option( 'yatoon_ai_frontend_font_size', (string) min( 24, max( 11, $frontend_font_size ) ) );
        $frontend_text_color = isset( $_POST['yatoon_ai_frontend_text_color'] ) ? sanitize_hex_color( wp_unslash( $_POST['yatoon_ai_frontend_text_color'] ) ) : '#111827';
        update_option( 'yatoon_ai_frontend_text_color', $frontend_text_color ?: '#111827' );
        $frontend_bg_color = isset( $_POST['yatoon_ai_frontend_bg_color'] ) ? sanitize_hex_color( wp_unslash( $_POST['yatoon_ai_frontend_bg_color'] ) ) : '#f8fafc';
        update_option( 'yatoon_ai_frontend_bg_color', $frontend_bg_color ?: '#f8fafc' );
        $display_mode = isset( $_POST['yatoon_ai_display_mode'] ) ? sanitize_key( wp_unslash( $_POST['yatoon_ai_display_mode'] ) ) : 'collapsed';
        update_option( 'yatoon_ai_display_mode', in_array( $display_mode, array( 'expanded', 'collapsed' ), true ) ? $display_mode : 'collapsed' );

        $allowed_providers = array( 'google', 'openai', 'anthropic' );
        $provider_order = array();
        if ( isset( $_POST['yatoon_ai_provider_order'] ) && is_array( $_POST['yatoon_ai_provider_order'] ) ) {
            foreach ( wp_unslash( $_POST['yatoon_ai_provider_order'] ) as $provider ) {
                $provider = sanitize_key( wp_unslash( $provider ) );
                if ( in_array( $provider, $allowed_providers, true ) && ! in_array( $provider, $provider_order, true ) ) {
                    $provider_order[] = $provider;
                }
            }
        }
        foreach ( $allowed_providers as $provider ) {
            if ( ! in_array( $provider, $provider_order, true ) ) {
                $provider_order[] = $provider;
            }
        }
        update_option( 'yatoon_ai_provider_order', $provider_order );

        $allowed_models = array(
            'google'    => array( 'gemini-1.5-flash', 'gemini-1.5-flash-8b', 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-2.5-flash', 'gemini-2.5-flash-lite' ),
            'openai'    => array( 'gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-5-mini', 'gpt-5-nano' ),
            'anthropic' => array( 'claude-3-5-haiku-20241022', 'claude-3-haiku-20240307', 'claude-3-5-sonnet-20241022', 'claude-3-7-sonnet-20250219' ),
        );
        $model_defaults = array(
            'google'    => 'gemini-1.5-flash',
            'openai'    => 'gpt-4o-mini',
            'anthropic' => 'claude-3-5-haiku-20241022',
        );
        foreach ( $allowed_models as $provider => $models ) {
            $posted = isset( $_POST[ 'yatoon_ai_model_' . $provider ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'yatoon_ai_model_' . $provider ] ) ) : $model_defaults[ $provider ];
            update_option( 'yatoon_ai_model_' . $provider, in_array( $posted, $models, true ) ? $posted : $model_defaults[ $provider ] );
        }

        wp_safe_redirect( add_query_arg( array(
            'page'  => 'yatoon-ai-assistant',
            'saved' => '1',
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    public function handle_save_email() {
        $this->save_options( array(
            'yatoon_send_confirmation_email', 'yatoon_email_notify_admin',
            'yatoon_email_subject', 'yatoon_email_template', 'yatoon_email_template_style',
            'yatoon_manage_booking_page_id', 'yatoon_staff_portal_page_id', 'yatoon_manage_token_days',
            'yatoon_manage_min_hours_before', 'yatoon_manage_allow_reschedule',
            'yatoon_manage_notify_admin_cancel', 'yatoon_manage_notify_admin_reschedule',
            'yatoon_reminder_enabled', 'yatoon_reminder_hours_before',
            'yatoon_reminder_subject', 'yatoon_reminder_template',
            'yatoon_review_enabled', 'yatoon_review_hours_after',
            'yatoon_review_subject', 'yatoon_review_template', 'yatoon_review_google_link',
            'yatoon_cancel_subject', 'yatoon_cancel_template',
            'yatoon_reschedule_subject', 'yatoon_reschedule_template',
            'yatoon_no_show_followup_enabled', 'yatoon_no_show_followup_hours_after',
            'yatoon_no_show_followup_subject', 'yatoon_no_show_followup_template',
        ), 'yatoon_save_email_settings', array(
            'yatoon_send_confirmation_email', 'yatoon_email_notify_admin',
            'yatoon_reminder_enabled', 'yatoon_review_enabled',
            'yatoon_manage_allow_reschedule', 'yatoon_manage_notify_admin_cancel',
            'yatoon_manage_notify_admin_reschedule', 'yatoon_no_show_followup_enabled',
        ) );
    }

    public function handle_save_sms() {
        $this->save_options( array(
            'yatoon_twilio_enabled', 'yatoon_twilio_account_sid', 'yatoon_twilio_auth_token',
            'yatoon_twilio_from_number', 'yatoon_twilio_admin_number',
            'yatoon_twilio_customer_sms', 'yatoon_twilio_customer_template',
            'yatoon_twilio_admin_sms', 'yatoon_twilio_admin_template',
            'yatoon_twilio_reminder_sms', 'yatoon_twilio_reminder_template',
            'yatoon_twilio_logs',
        ), 'yatoon_save_sms_settings', array(
            'yatoon_twilio_enabled', 'yatoon_twilio_customer_sms',
            'yatoon_twilio_admin_sms', 'yatoon_twilio_reminder_sms', 'yatoon_twilio_logs',
        ) );
    }

    public function handle_save_calendar() {
        $this->save_options( array(
            'yatoon_google_cal_enabled', 'yatoon_google_cal_calendar_id',
            'yatoon_google_cal_client_id', 'yatoon_google_cal_client_secret',
            'yatoon_google_cal_refresh_token', 'yatoon_google_cal_send_invites',
        ), 'yatoon_save_calendar_settings', array(
            'yatoon_google_cal_enabled', 'yatoon_google_cal_send_invites',
        ) );
    }

    public function handle_save_brand() {
        $this->save_options( array(
            'yatoon_business_name', 'yatoon_business_logo', 'yatoon_business_logo_id',
            'yatoon_primary_color', 'yatoon_secondary_color', 'yatoon_text_color', 'yatoon_body_text_color',
            'yatoon_price_color', 'yatoon_button_radius', 'yatoon_card_radius',
            'yatoon_plugin_page_bg_color', 'yatoon_plugin_card_bg_color', 'yatoon_plugin_border_color',
            'yatoon_primary_button_bg_color', 'yatoon_primary_button_text_color',
            'yatoon_primary_button_font_size', 'yatoon_primary_button_radius',
            'yatoon_font_family', 'sbs_button_radius', 'sbs_card_radius', 'sbs_font_family',
            'sbs_readmore_color', 'sbs_heading_font', 'sbs_heading_color',
            'sbs_heading_size', 'sbs_main_title_size',
            'yatoon_cat_tab_size', 'sbs_cat_tab_size',
            'yatoon_load_google_fonts',
            'yatoon_business_address', 'yatoon_business_phone', 'yatoon_business_website',
            'yatoon_contact_phone', 'yatoon_contact_email',
        ), 'yatoon_save_brand_settings', array(
            'yatoon_load_google_fonts',
        ) );
    }

    public function sanitize_setting($value) {
        if (is_array($value)) {
            return array_map(array($this, 'sanitize_setting'), $value);
        }

        if (is_bool($value) || is_numeric($value)) {
            return $value;
        }

        return sanitize_textarea_field(wp_unslash($value));
    }

    public function sanitize_service_id_list($value) {
        if (!is_array($value)) {
            $value = explode(',', (string) $value);
        }

        $ids = array();
        foreach ($value as $id) {
            $id = absint($id);
            if ($id > 0 && !in_array($id, $ids, true)) {
                $ids[] = $id;
            }
        }

        return $ids;
    }

    public function register_settings() {
        register_setting('yatoon_settings', 'yatoon_square_access_token', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_square_location_id', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_square_environment', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_square_booking_widget_id', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_booking_template', array('sanitize_callback' => 'sanitize_key'));
        register_setting('yatoon_settings', 'yatoon_show_advanced_menu', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_test_booking_mode', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_booking_sync_enabled',  array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_booking_sync_interval', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_auto_sync_enabled', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_sync_interval', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_use_hybrid_mode', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_pwa_enabled', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_send_confirmation_email', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_email_subject', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_email_template', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_contact_phone', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_business_phone', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_business_address', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_business_website', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_business_phone', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_business_address', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_business_website', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_book_new_url', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_url', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_category_order', array('type'=>'array','sanitize_callback'=>function($v){return is_array($v)?array_map('intval',$v):array();}));
        register_setting('yatoon_brand_settings', 'yatoon_category_order', array('sanitize_callback' => 'sanitize_textarea_field'));
        register_setting('yatoon_settings', 'yatoon_show_portal_banner', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_portal_banner_prompt', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_new_tab', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_primary_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_banner_bg', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_banner_text', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_login_title', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_login_brand', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_login_subtitle', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_show_venue_hero', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_venue_kicker', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_public_rating_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_venue_pay_later_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_venue_deposit_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_contact_email', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_text_us_enabled', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_text_us_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_waitlist_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_show_availability_source_note', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_show_success_next', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_success_next_title', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_success_pay_later_text', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_success_deposit_text', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_success_followup_text', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_success_action_note', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_rebooking_nudge_text', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_rebooking_nudge_btn',  array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_rating_enabled',       array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_rating_hours_after',   array('sanitize_callback' => 'absint'));
        register_setting('yatoon_settings', 'yatoon_rating_auto_approve',  array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_rating_subject',       array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_tip_enabled',   array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_tip_presets',   array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_tip_label',     array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_whatsapp_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_whatsapp_number',  array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_whatsapp_label',   array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_whatsapp_prefill', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_availability_help_text', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_load_google_fonts', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_load_external_phone_assets', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_use_ip_geo_phone_detect', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_price_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_banner_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_banner_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_banner_btn_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_textus_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_textus_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_slot_interval', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'sbs_header_offset', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_max_guests_together', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_settings', 'yatoon_booking_immersive_mode', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'sbs_business_hours', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_active_platform', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_vagaro_api_key', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_vagaro_business_id', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_consent_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_consent_title', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_consent_text', array('sanitize_callback' => 'sanitize_textarea_field'));
        register_setting('yatoon_settings', 'yatoon_consent_signature_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_policy_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_policy_require_ack', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_policy_title', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_policy_text', array('sanitize_callback' => 'sanitize_textarea_field'));
        register_setting('yatoon_settings', 'yatoon_policy_ack_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_waitlist_auto_notify', array('sanitize_callback' => 'sanitize_text_field'));

        // Customer self-service (manage booking)
        register_setting('yatoon_settings', 'yatoon_manage_booking_page_id', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_settings', 'yatoon_staff_portal_page_id', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_settings', 'yatoon_manage_token_days',      array('sanitize_callback' => 'absint'));
        register_setting('yatoon_settings', 'yatoon_manage_min_hours_before',array('sanitize_callback' => 'absint'));
        register_setting('yatoon_settings', 'yatoon_manage_allow_reschedule',array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_manage_notify_admin_cancel',    array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_manage_notify_admin_reschedule',array('sanitize_callback' => 'sanitize_text_field'));

        // Staff Calendar settings
        register_setting('yatoon_settings', 'yatoon_calendar_hidden_staff',  array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_calendar_timezone',      array('sanitize_callback' => 'sanitize_text_field'));

        // Deposit settings
        register_setting('yatoon_settings', 'yatoon_deposit_enabled',        array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_deposit_type',           array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_deposit_amount',         array('sanitize_callback' => 'floatval'));
        register_setting('yatoon_settings', 'yatoon_deposit_stripe_key',     array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_deposit_stripe_secret',  array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_payment_request_url',    array('sanitize_callback' => 'esc_url_raw'));
        register_setting('yatoon_settings', 'yatoon_square_checkout_url',    array('sanitize_callback' => 'esc_url_raw'));
        register_setting('yatoon_settings', 'yatoon_square_app_url',         array('sanitize_callback' => 'esc_url_raw'));
        register_setting('yatoon_settings', 'yatoon_square_checkout_note',   array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_square_no_show_fee_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_square_no_show_fee_amount',  array('sanitize_callback' => 'floatval'));
        register_setting('yatoon_settings', 'yatoon_square_no_show_fee_note',    array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_stripe_webhook_secret',  array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_square_webhook_signature_key', array('sanitize_callback' => 'sanitize_text_field'));
        
        // Brand settings
        register_setting('yatoon_brand_settings', 'yatoon_business_name', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_business_logo', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_business_logo_id', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_primary_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_secondary_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_plugin_page_bg_color', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_brand_settings', 'yatoon_plugin_card_bg_color', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_brand_settings', 'yatoon_plugin_border_color', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_brand_settings', 'yatoon_primary_button_bg_color', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_brand_settings', 'yatoon_primary_button_text_color', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_brand_settings', 'yatoon_primary_button_font_size', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_brand_settings', 'yatoon_primary_button_radius', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_brand_settings', 'sbs_readmore_color', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_brand_settings', 'sbs_font_family', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_heading_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_heading_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_heading_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_main_title_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_cat_tab_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_cat_tab_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_show_popular_picks', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_brand_settings', 'yatoon_popular_picks_mobile_count', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_brand_settings', 'yatoon_popular_picks_desktop_count', array('sanitize_callback' => 'absint'));
        register_setting('yatoon_brand_settings', 'yatoon_popular_pick_service_ids', array('sanitize_callback' => array($this, 'sanitize_service_id_list')));
        register_setting('yatoon_brand_settings', 'yatoon_service_popular_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_customize_kicker', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_related_eyebrow', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_related_title', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_related_hint', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_main_group_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_main_group_help', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_addon_group_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_addon_group_help', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_recommended_badge_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_popular_badge_label', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_brand_settings', 'yatoon_option_addon_badge_label', array('sanitize_callback' => 'sanitize_text_field'));
        // Button shared font
        register_setting('yatoon_brand_settings', 'sbs_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        // Per-button fonts
        register_setting('yatoon_brand_settings', 'yatoon_add_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_back_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_continue_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        // ADD / service button
        register_setting('yatoon_brand_settings', 'yatoon_add_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_add_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_add_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_add_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_add_btn_radius', array('sanitize_callback' => 'absint'));
        // Back button
        register_setting('yatoon_brand_settings', 'yatoon_back_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_back_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_back_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_back_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'yatoon_back_btn_radius', array('sanitize_callback' => 'absint'));
        // Continue button
        register_setting('yatoon_brand_settings', 'sbs_continue_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_continue_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_continue_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_continue_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_continue_btn_radius', array('sanitize_callback' => 'absint'));
        // Add More Services button
        register_setting('yatoon_brand_settings', 'yatoon_addmore_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore_btn_radius', array('sanitize_callback' => 'absint'));
        // Add More Services button (calendar page, step 4)
        register_setting('yatoon_brand_settings', 'sbs_addmore2_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore2_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore2_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore2_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore2_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore2_btn_radius', array('sanitize_callback' => 'absint'));
        // Add More Services button (green banner top of calendar, step 4)
        register_setting('yatoon_brand_settings', 'sbs_addmore3_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore3_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore3_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore3_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore3_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_addmore3_btn_radius', array('sanitize_callback' => 'absint'));

        register_setting('yatoon_brand_settings', 'sbs_add_for_person_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_for_person_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_for_person_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_for_person_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_for_person_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_for_person_btn_radius', array('sanitize_callback' => 'absint'));

        register_setting('yatoon_brand_settings', 'sbs_add_guest_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_guest_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_guest_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_guest_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_guest_btn_font', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_add_guest_btn_radius', array('sanitize_callback' => 'absint'));
        // Confirm / Submit button
        register_setting('yatoon_brand_settings', 'sbs_confirm_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_btn_radius', array('sanitize_callback' => 'absint'));
        // Confirm single (1 appointment)
        register_setting('yatoon_brand_settings', 'sbs_confirm_single_btn_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_single_btn_size', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_single_btn_text_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_single_btn_bg_color', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_brand_settings', 'sbs_confirm_single_btn_radius', array('sanitize_callback' => 'absint'));
    }
    
    /**
     * Enqueue admin scripts
     */
    public function enqueue_admin_scripts($hook) {
        $page_slug = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
        $is_booking_page = class_exists('YATOON_App_Shell') ? YATOON_App_Shell::is_booking_admin_page() : (strpos($page_slug, 'sbs-') === 0 || $page_slug === 'yatoon-booking-system');
        if (!$is_booking_page) {
            return;
        }
        
        $min_admin = ( ! defined('SCRIPT_DEBUG') || ! SCRIPT_DEBUG ) && file_exists( SBS_PLUGIN_DIR . 'assets/css/admin.min.css' ) ? '.min' : '';
        $shell_asset_file = SBS_PLUGIN_DIR . 'assets/css/yatoon-app-shell' . YATOON_ASSET_SUFFIX . '.css';
        $ux_asset_file    = SBS_PLUGIN_DIR . 'assets/js/yatoon-ux' . YATOON_ASSET_SUFFIX . '.js';
        $admin_asset_version = file_exists($shell_asset_file) ? SBS_VERSION . '-admin-ui-5.' . filemtime($shell_asset_file) : SBS_VERSION . '-admin-ui-5';
        $ux_asset_version    = file_exists($ux_asset_file) ? SBS_VERSION . '.' . filemtime($ux_asset_file) : SBS_VERSION;
        wp_enqueue_style('sbs-admin-css', SBS_PLUGIN_URL . 'assets/css/admin' . $min_admin . '.css', array(), $admin_asset_version);
        wp_enqueue_style('yatoon-apple-polish', SBS_PLUGIN_URL . 'assets/css/apple-polish' . YATOON_ASSET_SUFFIX . '.css', array('sbs-admin-css'), $admin_asset_version);
        wp_enqueue_style('yatoon-ui-polish', SBS_PLUGIN_URL . 'assets/css/yatoon-ui-polish' . YATOON_ASSET_SUFFIX . '.css', array('yatoon-apple-polish'), $admin_asset_version);
        wp_add_inline_style('yatoon-apple-polish', yatoon_apple_polish_inline_css());
        wp_enqueue_script('sbs-admin-js', SBS_PLUGIN_URL . 'assets/js/admin' . YATOON_ASSET_SUFFIX . '.js', array('jquery'), $admin_asset_version, true);
        wp_enqueue_script('yatoon-admin-ui-js', SBS_PLUGIN_URL . 'assets/js/admin-ui' . YATOON_ASSET_SUFFIX . '.js', array('jquery', 'sbs-admin-js'), $admin_asset_version, true);

        // v3.5.0 app shell (independent left nav + top bar)
        wp_enqueue_style('yatoon-app-shell-css', SBS_PLUGIN_URL . 'assets/css/yatoon-app-shell' . YATOON_ASSET_SUFFIX . '.css', array('sbs-admin-css'), $admin_asset_version);
        wp_enqueue_style('yatoon-design-system', SBS_PLUGIN_URL . 'assets/css/yatoon-design-system' . YATOON_ASSET_SUFFIX . '.css', array('yatoon-ui-polish', 'yatoon-app-shell-css'), $admin_asset_version);
        wp_enqueue_script('yatoon-app-shell-js', SBS_PLUGIN_URL . 'assets/js/yatoon-app-shell' . YATOON_ASSET_SUFFIX . '.js', array('jquery'), $admin_asset_version, true);
        wp_enqueue_script('yatoon-ux', SBS_PLUGIN_URL . 'assets/js/yatoon-ux' . YATOON_ASSET_SUFFIX . '.js', array('yatoon-app-shell-js'), $ux_asset_version, true);
        wp_enqueue_script('yatoon-state-persist-js', SBS_PLUGIN_URL . 'assets/js/yatoon-state-persist' . YATOON_ASSET_SUFFIX . '.js', array('jquery'), $admin_asset_version, true);
        if ( class_exists( 'YATOON_App_Shell' ) ) {
            wp_localize_script('yatoon-app-shell-js', 'yatoonAppShell', array(
                'searchIndex'   => YATOON_App_Shell::get_search_index(),
                'noResultsText' => __('No matching pages.', 'yatoon-booking-system'),
            ));
        }
        
        // Load WordPress media library (needed for logo upload)
        wp_enqueue_media();
        
        // Services and Brand pages require jQuery UI Sortable
        if (strpos($hook, 'sbs-services') !== false || strpos($hook, 'sbs-brand') !== false || strpos($hook, 'yatoon-automations') !== false) {
            wp_enqueue_script('jquery-ui-sortable');
        }
        
        wp_localize_script('sbs-admin-js', 'sbsAdmin', array(
            'ajax_url' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('yatoon_admin_nonce'),
            'sms_enabled' => class_exists('YATOON_Twilio') && YATOON_Twilio::is_enabled(),
            'strings' => array(
                'confirm_delete' => __('Are you sure you want to delete this item?', 'yatoon-booking-system'),
                'sync_success' => __('Sync completed successfully!', 'yatoon-booking-system'),
                'sync_error' => __('Sync failed. Please check your settings.', 'yatoon-booking-system'),
            )
        ));
    }
    
    public function display_wizard_page() {
        include SBS_PLUGIN_DIR . 'admin/views/setup-wizard.php';
    }

    /**
     * Toggle one of the small set of boolean "Included Add-ons" switches
     * on the Add-ons page (v3.5.0). Whitelisted on purpose — this endpoint
     * must never be able to write an arbitrary option name.
     */
    public function ajax_toggle_addon_setting() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $allowed = array(
            'yatoon_auto_sync_enabled',
            'yatoon_pwa_enabled',
            'yatoon_ai_reply_drafts_enabled',
        );

        $option = isset($_POST['option']) ? sanitize_key( wp_unslash( $_POST['option'] ) ) : '';
        $value  = isset($_POST['value']) && $_POST['value'] === '1' ? '1' : '0';

        if (!in_array($option, $allowed, true)) {
            wp_send_json_error('Unknown option');
        }

        update_option($option, $value);
        wp_send_json_success(array('option' => $option, 'value' => $value));
    }

    public function display_addons_page() {
        include SBS_PLUGIN_DIR . 'admin/views/addons.php';
    }

    private function ensure_shortcode_page( $option_key, $title, $shortcode, $selected_page_id = 0 ) {
        $page_id = absint( $selected_page_id );
        if ( !$page_id ) {
            $saved_page_id = absint( get_option( $option_key, 0 ) );
            if ( $saved_page_id && 'page' === get_post_type( $saved_page_id ) ) {
                $page_id = $saved_page_id;
            }
        }
        if ( $page_id ) {
            $page = get_post( $page_id );
            if ( !$page || 'page' !== $page->post_type ) {
                $page_id = 0;
            } elseif ( !has_shortcode( $page->post_content, trim( $shortcode, '[]' ) ) ) {
                wp_update_post( array( 'ID' => $page_id, 'post_content' => rtrim( $page->post_content ) . "\n\n" . $shortcode ) );
            }
        }
        if ( !$page_id ) {
            $page_id = wp_insert_post( array( 'post_title' => $title, 'post_content' => $shortcode, 'post_status' => 'publish', 'post_type' => 'page' ), true );
            if ( is_wp_error( $page_id ) ) {
                return 0;
            }
        }
        update_option( $option_key, absint( $page_id ) );
        return absint( $page_id );
    }

    private function record_onboarding_milestone_step( $milestone ) {
        yatoon_record_onboarding_milestone( $milestone );
    }

    public function show_wizard_notice() {
        if ( '1' === get_option( 'yatoon_wizard_dismissed', '0' ) || '1' === get_option( 'yatoon_wizard_done', '0' ) ) return;
        if ( class_exists( 'YATOON_App_Shell' ) && YATOON_App_Shell::is_booking_admin_page() ) return;
        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
        if ( ! $screen || 'dashboard' !== (string) $screen->id ) return;
        echo '<div class="notice notice-info" style="border-left-color:#2563eb;padding:12px 16px;display:flex;align-items:center;gap:14px;">';
        echo '<div><strong>' . esc_html__( 'Finish setting up Yatoon Booking', 'yatoon-booking-system' ) . '</strong><br><span>' . esc_html__( 'Create your booking pages and test the customer flow in a few minutes.', 'yatoon-booking-system' ) . '</span></div>';
        echo '<a class="button button-primary" href="' . esc_url( admin_url( 'admin.php?page=yatoon-setup-wizard' ) ) . '">' . esc_html__( 'Open setup wizard', 'yatoon-booking-system' ) . '</a>';
        echo '<a href="' . esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=yatoon_dismiss_wizard' ), 'yatoon_dismiss_wizard' ) ) . '" style="margin-left:auto;color:#64748b;text-decoration:none;font-size:20px;" aria-label="' . esc_attr__( 'Dismiss setup notice', 'yatoon-booking-system' ) . '">&times;</a></div>';
    }

    public function handle_wizard_save() {
        if ( !current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'You are not allowed to manage booking settings.', 'yatoon-booking-system' ) );
        check_admin_referer( 'yatoon_wizard_save', 'yatoon_wizard_nonce' );
        $step = max( 1, min( 4, absint( $_POST['wizard_step'] ?? 1 ) ) );
        $skip = !empty( $_POST['skip_step'] );
        if ( !$skip && 1 === $step ) {
            $phone = sanitize_text_field( wp_unslash( $_POST['yatoon_business_phone'] ?? '' ) );
            update_option( 'yatoon_business_name', sanitize_text_field( wp_unslash( $_POST['yatoon_business_name'] ?? '' ) ) );
            update_option( 'yatoon_business_phone', $phone ); update_option( 'sbs_business_phone', $phone );
            update_option( 'yatoon_contact_email', sanitize_email( wp_unslash( $_POST['yatoon_contact_email'] ?? '' ) ) );
            update_option( 'yatoon_business_address', sanitize_text_field( wp_unslash( $_POST['yatoon_business_address'] ?? '' ) ) );
            update_option( 'yatoon_business_website', esc_url_raw( wp_unslash( $_POST['yatoon_business_website'] ?? '' ) ) );
            $this->record_onboarding_milestone_step( 'business_saved' );
        } elseif ( !$skip && 2 === $step ) {
            $platform = sanitize_key( wp_unslash( $_POST['yatoon_active_platform'] ?? 'local' ) );
            if ( !in_array( $platform, array( 'local','square','vagaro' ), true ) ) $platform = 'local';
            update_option( 'yatoon_active_platform', $platform ); update_option( 'yatoon_platform', $platform );
            if ( 'square' === $platform ) {
                if ( !empty( $_POST['yatoon_square_access_token'] ) ) YATOON_Crypto::update_option( 'yatoon_square_access_token', sanitize_text_field( wp_unslash( $_POST['yatoon_square_access_token'] ) ) );
                update_option( 'yatoon_square_location_id', sanitize_text_field( wp_unslash( $_POST['yatoon_square_location_id'] ?? '' ) ) );
            } elseif ( 'vagaro' === $platform ) {
                if ( !empty( $_POST['yatoon_vagaro_api_key'] ) ) YATOON_Crypto::update_option( 'yatoon_vagaro_api_key', sanitize_text_field( wp_unslash( $_POST['yatoon_vagaro_api_key'] ) ) );
                update_option( 'yatoon_vagaro_business_id', sanitize_text_field( wp_unslash( $_POST['yatoon_vagaro_business_id'] ?? '' ) ) );
            }
            if ( 'demo' === sanitize_key( wp_unslash( $_POST['yatoon_starter_mode'] ?? 'blank' ) ) ) {
                YATOON_Activator::seed_starter_data(); update_option( 'yatoon_starter_data_seeded', 'selected' );
            } else { update_option( 'yatoon_starter_data_seeded', 'skipped' ); }
            $this->record_onboarding_milestone_step( 'mode_selected' );
        } elseif ( !$skip && 3 === $step ) {
            foreach ( array( 'monday','tuesday','wednesday','thursday','friday','saturday','sunday' ) as $day ) {
                update_option( "yatoon_hours_{$day}", isset( $_POST[ "yatoon_closed_{$day}" ] ) ? 'closed' : sanitize_text_field( wp_unslash( $_POST[ "yatoon_hours_{$day}" ] ?? '' ) ) );
            }
            update_option( 'yatoon_send_confirmation_email', isset( $_POST['yatoon_send_confirmation_email'] ) ? '1' : '0' );
            update_option( 'yatoon_reminder_enabled', isset( $_POST['yatoon_reminder_enabled'] ) ? '1' : '0' );
            update_option( 'yatoon_email_notify_admin', isset( $_POST['yatoon_email_notify_admin'] ) ? '1' : '0' );
            update_option( 'yatoon_reminder_hours_before', max( 1, min( 72, absint( $_POST['yatoon_reminder_hours_before'] ?? 24 ) ) ) );
            $this->record_onboarding_milestone_step( 'hours_saved' );
        } elseif ( !$skip && 4 === $step ) {
            $booking_page_id = $this->ensure_shortcode_page( 'yatoon_booking_page_id', __( 'Book an Appointment', 'yatoon-booking-system' ), '[yatoon_booking]', absint( $_POST['yatoon_booking_page_id'] ?? 0 ) );
            if ( !empty( $_POST['yatoon_create_support_pages'] ) ) {
                $this->ensure_shortcode_page( 'yatoon_manage_booking_page_id', __( 'Manage Your Appointment', 'yatoon-booking-system' ), '[yatoon_manage_booking]' );
                $portal_page_id = $this->ensure_shortcode_page( 'yatoon_customer_portal_page_id', __( 'My Appointments', 'yatoon-booking-system' ), '[yatoon_customer_portal]' );
                if ( $portal_page_id ) update_option( 'yatoon_portal_url', get_permalink( $portal_page_id ) );
            }
            $color = sanitize_hex_color( wp_unslash( $_POST['yatoon_primary_color'] ?? '' ) );
            update_option( 'yatoon_primary_color', $color ? $color : '#ec4899' );
            update_option( 'yatoon_min_advance_hours', max( 0, min( 72, absint( $_POST['yatoon_min_advance_hours'] ?? 2 ) ) ) );
            update_option( 'yatoon_wizard_done', '1' );
            $this->record_onboarding_milestone_step( 'booking_page_ready' );
            $this->record_onboarding_milestone_step( 'wizard_completed' );
            if ( $booking_page_id ) update_option( 'yatoon_onboarding_booking_page_url', get_permalink( $booking_page_id ) );
        }
        wp_safe_redirect( admin_url( 'admin.php?page=yatoon-setup-wizard&wizard_step=' . ( 4 === $step ? 5 : $step + 1 ) ) ); exit;
    }

    public function handle_wizard_dismiss() {
        if ( !current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'You are not allowed to manage booking settings.', 'yatoon-booking-system' ) );
        check_admin_referer( 'yatoon_dismiss_wizard' ); update_option( 'yatoon_wizard_dismissed', '1' );
        wp_safe_redirect( admin_url( 'admin.php?page=yatoon-booking-system' ) ); exit;
    }

    public function handle_start_test_booking() {
        if ( !current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'You are not allowed to start a test booking.', 'yatoon-booking-system' ) );
        }
        check_admin_referer( 'yatoon_start_test_booking' );
        $page_id = absint( get_option( 'yatoon_booking_page_id', 0 ) );
        $url = $page_id ? get_permalink( $page_id ) : '';
        if ( !$url ) {
            wp_safe_redirect( admin_url( 'admin.php?page=yatoon-setup-wizard&wizard_step=4' ) );
            exit;
        }
        update_option( 'yatoon_test_booking_mode', '1' );
        wp_safe_redirect( add_query_arg( 'yatoon_test_booking', '1', $url ) );
        exit;
    }

    public function display_dashboard_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        global $wpdb;
        $bookings_table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        $services_table = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $staff_table    = esc_sql( $wpdb->prefix . 'yatoon_staff' );
        $clients_table  = esc_sql( $wpdb->prefix . 'yatoon_clients' );
        $today          = current_time('Y-m-d');
        $now_mysql      = current_time('mysql');
        $week_mysql     = gmdate('Y-m-d H:i:s', strtotime('+7 days', current_time('timestamp')));
        $risk_cutoff    = gmdate('Y-m-d H:i:s', strtotime('-60 days', current_time('timestamp')));
        $month_start    = gmdate('Y-m-01', current_time('timestamp'));
        $last_month_start = gmdate('Y-m-01', strtotime('-1 month', current_time('timestamp')));
        $last_month_end   = gmdate('Y-m-t', strtotime('-1 month', current_time('timestamp')));

        $service_count = (int) yatoon_db_get_var("SELECT COUNT(*) FROM " . $services_table . " WHERE is_active = 1");
        $staff_count   = (int) yatoon_db_get_var("SELECT COUNT(*) FROM " . $staff_table . " WHERE is_active = 1 AND is_bookable = 1");
        $booking_page_id = (int) get_option('yatoon_booking_page_id', 0);
        $booking_page_ok = $booking_page_id > 0 && get_post_status($booking_page_id) === 'publish';
        $booking_preview_url = $booking_page_ok ? get_permalink($booking_page_id) : '';
        if (!$booking_preview_url) {
            $booking_preview_page = get_page_by_path('booking');
            $booking_preview_url = $booking_preview_page ? get_permalink($booking_preview_page->ID) : '';
        }
        $platform = sanitize_key((string) get_option('yatoon_active_platform', 'local'));
        $platform_ready = true;
        $platform_label = 'Local mode';
        if ($platform === 'square') {
            $platform_label = 'Square sync';
            $platform_ready = YATOON_Crypto::get_option('yatoon_square_access_token', '') !== '' && get_option('yatoon_square_location_id', '') !== '';
        } elseif ($platform === 'vagaro') {
            $platform_label = 'Vagaro sync';
            $platform_ready = YATOON_Crypto::get_option('yatoon_vagaro_api_key', '') !== '' && get_option('yatoon_vagaro_business_id', '') !== '';
        }
        $notifications_ready = get_option('yatoon_send_confirmation_email', '1') === '1' || get_option('yatoon_twilio_enabled', '0') === '1';
        /* translators: %d: number of active services. */
        $service_count_label = sprintf(_n('%d service', '%d services', $service_count, 'yatoon-booking-system'), $service_count);
        /* translators: %d: number of bookable staff members. */
        $staff_count_label = sprintf(_n('%d staff member', '%d staff members', $staff_count, 'yatoon-booking-system'), $staff_count);
        $setup_checklist = array(
            array(
                'label' => __('Active services', 'yatoon-booking-system'),
                'ok'    => $service_count > 0,
                'meta'  => $service_count_label,
                'url'   => admin_url('admin.php?page=sbs-services'),
            ),
            array(
                'label' => __('Bookable staff', 'yatoon-booking-system'),
                'ok'    => $staff_count > 0,
                'meta'  => $staff_count_label,
                'url'   => admin_url('admin.php?page=sbs-staff'),
            ),
            array(
                'label' => __('Booking page', 'yatoon-booking-system'),
                'ok'    => $booking_page_ok,
                'meta'  => $booking_page_ok ? __('Published', 'yatoon-booking-system') : __('Create or select a page', 'yatoon-booking-system'),
                'url'   => admin_url('admin.php?page=yatoon-booking-system'),
            ),
            array(
                'label' => $platform_label,
                'ok'    => $platform_ready,
                'meta'  => $platform_ready ? __('Ready', 'yatoon-booking-system') : __('Needs credentials', 'yatoon-booking-system'),
                'url'   => admin_url('admin.php?page=yatoon-booking-system'),
            ),
            array(
                'label' => __('Customer notifications', 'yatoon-booking-system'),
                'ok'    => $notifications_ready,
                'meta'  => $notifications_ready ? __('Enabled', 'yatoon-booking-system') : __('Email or SMS is off', 'yatoon-booking-system'),
                'url'   => admin_url('admin.php?page=sbs-notifications'),
            ),
        );
        $setup_ready_count = count(array_filter($setup_checklist, function($item) {
            return !empty($item['ok']);
        }));

        $dashboard_summary = yatoon_db_get_row($wpdb->prepare(
            "SELECT COUNT(id) AS today_bookings,
                    SUM(CASE WHEN status IN ('pending','confirmed','ACCEPTED') THEN 1 ELSE 0 END) AS today_active,
                    SUM(CASE WHEN payment_status IS NULL OR payment_status = 'unpaid' THEN 1 ELSE 0 END) AS today_unpaid,
                    SUM(CASE WHEN status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' THEN total_price ELSE 0 END) AS today_revenue
             FROM " . $bookings_table . "
             WHERE DATE(start_time) = %s",
            $today
        ));

        // Month-to-date stats
        $mtd_summary = yatoon_db_get_row($wpdb->prepare(
            "SELECT COUNT(id) AS total_bookings,
                    SUM(CASE WHEN status = 'no_show' THEN 1 ELSE 0 END) AS no_show_count,
                    SUM(CASE WHEN status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' AND status != 'no_show' THEN total_price ELSE 0 END) AS revenue
             FROM " . $bookings_table . "
             WHERE start_time >= %s AND start_time < %s",
            $month_start . ' 00:00:00',
            $now_mysql
        ));

        // Last month revenue for comparison
        $last_month_revenue = yatoon_db_get_var($wpdb->prepare(
            "SELECT SUM(total_price) FROM " . $bookings_table . "
             WHERE start_time >= %s AND start_time <= %s
               AND status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' AND status != 'no_show'",
            $last_month_start . ' 00:00:00',
            $last_month_end . ' 23:59:59'
        ));

        // 7-day daily trend
        $weekly_trend = yatoon_db_get_results($wpdb->prepare(
            "SELECT DATE(start_time) AS day,
                    COUNT(id) AS bookings,
                    SUM(CASE WHEN status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' AND status != 'no_show' THEN total_price ELSE 0 END) AS revenue
             FROM " . $bookings_table . "
             WHERE start_time >= DATE_SUB(%s, INTERVAL 6 DAY) AND start_time < DATE_ADD(%s, INTERVAL 1 DAY)
             GROUP BY DATE(start_time)
             ORDER BY day ASC",
            $today,
            $today
        ));

        // No-show risk clients (3+ no-shows)
        $noshow_risk_clients = yatoon_db_get_var(
            "SELECT COUNT(DISTINCT customer_email) FROM " . $bookings_table . "
             WHERE status = 'no_show' AND customer_email != ''
             GROUP BY customer_email HAVING COUNT(*) >= 3"
        );

        $today_bookings = yatoon_db_get_results($wpdb->prepare(
            "SELECT b.*, s.name AS service_name, st.name AS staff_name
             FROM " . $bookings_table . " b
             LEFT JOIN " . $services_table . " s ON b.service_id = s.id
             LEFT JOIN " . $staff_table . " st ON b.staff_id = st.id
             WHERE DATE(b.start_time) = %s
             ORDER BY b.start_time ASC
             LIMIT 20",
            $today
        ));

        $todo_unpaid = yatoon_db_get_results(
            "SELECT b.id, b.customer_name, b.customer_email, b.customer_phone, b.start_time, b.total_price, s.name AS service_name
             FROM " . $bookings_table . " b
             LEFT JOIN " . $services_table . " s ON b.service_id = s.id
             WHERE (b.payment_status IS NULL OR b.payment_status = 'unpaid')
               AND b.status NOT LIKE 'cancelled%'
               AND b.status NOT LIKE 'CANCELLED%'
               AND b.status != 'DECLINED'
             ORDER BY b.start_time DESC
             LIMIT 8"
        );

        $todo_recall = yatoon_db_get_results($wpdb->prepare(
            "SELECT c.customer_name, c.customer_email, c.customer_phone, MAX(b.start_time) AS last_visit, COUNT(b.id) AS visit_count
             FROM " . $clients_table . " c
             INNER JOIN " . $bookings_table . " b ON (b.customer_phone = c.customer_phone OR b.customer_email = c.customer_email)
             WHERE b.status NOT LIKE 'cancelled%%'
               AND b.status NOT LIKE 'CANCELLED%%'
               AND b.status != 'DECLINED'
             GROUP BY c.id
             HAVING last_visit < %s
                AND SUM(CASE WHEN b.start_time >= %s AND b.start_time < %s AND b.status IN ('pending','confirmed','ACCEPTED') THEN 1 ELSE 0 END) = 0
             ORDER BY last_visit ASC
             LIMIT 8",
            $risk_cutoff,
            $now_mysql,
            $week_mysql
        ));

        include SBS_PLUGIN_DIR . 'admin/views/dashboard.php';
    }

    /**
     * Display settings page
     */
    public function display_settings_page() {
        if (!current_user_can('manage_options')) {
            return;
        }
        
        include SBS_PLUGIN_DIR . 'admin/views/settings.php';
    }

    public function display_ai_assistant_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        include SBS_PLUGIN_DIR . 'admin/views/ai-assistant.php';
    }
    
    /**
     * Display services page
     */
    public function display_services_page() {
        if (!current_user_can('manage_options')) {
            return;
        }
        
        global $wpdb;
        $table_name = esc_sql( $wpdb->prefix . 'yatoon_services' );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $services = yatoon_db_get_results("SELECT * FROM " . $table_name . " ORDER BY sort_order ASC, name ASC");

        include SBS_PLUGIN_DIR . 'admin/views/services.php';
    }

    /**
     * Display staff page
     */
    public function display_staff_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        global $wpdb;
        $table_name = esc_sql( $wpdb->prefix . 'yatoon_staff' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $staff = yatoon_db_get_results("SELECT * FROM " . $table_name . " ORDER BY sort_order ASC, name ASC");

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
        $yatoon_staff_is_pro    = class_exists( 'YATOON_License' ) && YATOON_License::is_pro();
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
        $yatoon_staff_limit     = 2; // Free plan cap
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
        $yatoon_staff_at_limit  = ! $yatoon_staff_is_pro && count( $staff ) >= $yatoon_staff_limit;

        include SBS_PLUGIN_DIR . 'admin/views/staff.php';
    }

    /**
     * Display bookings page
     */
    public function display_bookings_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        global $wpdb;
        $table_bookings = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        $table_services = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $table_staff = esc_sql( $wpdb->prefix . 'yatoon_staff' );
        $table_clients = esc_sql( $wpdb->prefix . 'yatoon_clients' );

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $bookings = yatoon_db_get_results("
            SELECT
                b.*,
                s.name as service_name,
                st.name as staff_name,
                c.id as client_id
            FROM " . $table_bookings . " b
            LEFT JOIN " . $table_services . " s ON b.service_id = s.id
            LEFT JOIN " . $table_staff . " st ON b.staff_id = st.id
            LEFT JOIN " . $table_clients . " c ON c.id = (
                SELECT c2.id FROM " . $table_clients . " c2
                WHERE (c2.customer_email != '' AND c2.customer_email = b.customer_email)
                   OR (c2.customer_phone != '' AND c2.customer_phone = b.customer_phone)
                ORDER BY c2.updated_at DESC LIMIT 1
            )
            ORDER BY b.start_time DESC
            LIMIT 100
        ");
        
        include SBS_PLUGIN_DIR . 'admin/views/bookings.php';
    }

    /**
     * Display staff calendar page
     */
    public function display_calendar_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        global $wpdb;
        $date = isset($_GET['date']) ? sanitize_text_field(wp_unslash($_GET['date'])) : current_time('Y-m-d');
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $date = current_time('Y-m-d');
        }

        $staff = yatoon_db_get_results("SELECT * FROM {$wpdb->prefix}yatoon_staff WHERE is_active = 1 AND is_bookable = 1 ORDER BY sort_order ASC, name ASC"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

        // Filter out staff hidden from calendar view
        $hidden_staff_raw = get_option( 'yatoon_calendar_hidden_staff', '' );
        $hidden_staff_ids = array_filter( array_map( 'intval', explode( ',', $hidden_staff_raw ) ) );
        if ( ! empty( $hidden_staff_ids ) ) {
            $staff = array_values( array_filter( $staff, function( $s ) use ( $hidden_staff_ids ) {
                return ! in_array( intval( $s->id ), $hidden_staff_ids, true );
            } ) );
        }

        $bookings = yatoon_db_get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT b.*, s.name AS service_name, st.name AS staff_name
             FROM {$wpdb->prefix}yatoon_bookings b
             LEFT JOIN {$wpdb->prefix}yatoon_services s ON b.service_id = s.id
             LEFT JOIN {$wpdb->prefix}yatoon_staff st ON b.staff_id = st.id
             WHERE DATE(b.start_time) = %s
             AND b.status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED')
             ORDER BY b.start_time ASC",
            $date
        ));

        include SBS_PLUGIN_DIR . 'admin/views/calendar.php';
    }

    /**
     * Display reports page
     */
    public function display_reports_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        global $wpdb;
        $date_from = isset($_GET['date_from']) ? sanitize_text_field(wp_unslash($_GET['date_from'])) : gmdate('Y-m-d', strtotime('-29 days', current_time('timestamp')));
        $date_to = isset($_GET['date_to']) ? sanitize_text_field(wp_unslash($_GET['date_to'])) : current_time('Y-m-d');

        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from)) {
            $date_from = gmdate('Y-m-d', strtotime('-29 days', current_time('timestamp')));
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to)) {
            $date_to = current_time('Y-m-d');
        }
        if ($date_from > $date_to) {
            $tmp = $date_from;
            $date_from = $date_to;
            $date_to = $tmp;
        }

        $bookings_table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        $services_table = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $staff_table = esc_sql( $wpdb->prefix . 'yatoon_staff' );
        $like_cancelled_lower = 'cancelled%';
        $like_cancelled_upper = 'CANCELLED%';

        $summary = yatoon_db_get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT
                COUNT(id) AS total_bookings,
                SUM(CASE WHEN status IN ('completed','COMPLETED') THEN 1 ELSE 0 END) AS completed_count,
                SUM(CASE WHEN status IN ('pending','confirmed','ACCEPTED') THEN 1 ELSE 0 END) AS upcoming_count,
                SUM(CASE WHEN status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                SUM(CASE WHEN status LIKE 'cancelled%%' OR status LIKE 'CANCELLED%%' OR status = 'DECLINED' THEN 1 ELSE 0 END) AS cancelled_count,
                SUM(CASE WHEN payment_status IS NULL OR payment_status = 'unpaid' THEN 1 ELSE 0 END) AS unpaid_count,
                SUM(CASE WHEN payment_status = 'paid_square_pos' THEN total_price ELSE 0 END) AS paid_square_pos_total,
                SUM(CASE WHEN status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' THEN total_price ELSE 0 END) AS scheduled_revenue
             FROM " . $bookings_table . "
             WHERE DATE(start_time) BETWEEN %s AND %s",
            $date_from, $date_to
        ));

        $today = current_time('Y-m-d');
        $today_summary = yatoon_db_get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT
                COUNT(id) AS booking_count,
                SUM(CASE WHEN status IN ('pending','confirmed','ACCEPTED') THEN 1 ELSE 0 END) AS active_count,
                SUM(CASE WHEN status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                SUM(CASE WHEN payment_status IS NULL OR payment_status = 'unpaid' THEN 1 ELSE 0 END) AS unpaid_count,
                SUM(CASE WHEN status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' THEN total_price ELSE 0 END) AS scheduled_revenue
             FROM " . $bookings_table . "
             WHERE DATE(start_time) = %s",
            $today
        ));

        $top_services = yatoon_db_get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT s.name,
                    COUNT(b.id) AS booking_count,
                    SUM(CASE WHEN b.status IN ('completed','COMPLETED') THEN 1 ELSE 0 END) AS completed_count,
                    SUM(b.total_price) AS total_revenue
             FROM " . $bookings_table . " b
             LEFT JOIN " . $services_table . " s ON b.service_id = s.id
             WHERE DATE(b.start_time) BETWEEN %s AND %s
             GROUP BY b.service_id, s.name
             ORDER BY booking_count DESC, total_revenue DESC
             LIMIT 10",
            $date_from, $date_to
        ));

        $staff_performance = yatoon_db_get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT st.name,
                    COUNT(b.id) AS booking_count,
                    SUM(CASE WHEN b.status IN ('completed','COMPLETED') THEN 1 ELSE 0 END) AS completed_count,
                    SUM(CASE WHEN b.status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                    SUM(b.total_price) AS total_revenue
             FROM " . $bookings_table . " b
             LEFT JOIN " . $staff_table . " st ON b.staff_id = st.id
             WHERE DATE(b.start_time) BETWEEN %s AND %s
             GROUP BY b.staff_id, st.name
             ORDER BY booking_count DESC, total_revenue DESC
             LIMIT 10",
            $date_from, $date_to
        ));

        $range_days = max(1, (int) ((strtotime($date_to) - strtotime($date_from)) / DAY_IN_SECONDS) + 1);
        $staff_capacity_minutes = $range_days * 8 * 60;
        $staff_utilization = yatoon_db_get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT st.name,
                    COUNT(b.id) AS booking_count,
                    SUM(CASE
                        WHEN b.status NOT LIKE 'cancelled%%' AND b.status NOT LIKE 'CANCELLED%%' AND b.status != 'DECLINED'
                        THEN TIMESTAMPDIFF(MINUTE, b.start_time, b.end_time)
                        ELSE 0
                    END) AS booked_minutes,
                    SUM(CASE WHEN b.status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                    SUM(b.total_price) AS total_revenue
             FROM " . $staff_table . " st
             LEFT JOIN " . $bookings_table . " b ON b.staff_id = st.id AND DATE(b.start_time) BETWEEN %s AND %s
             WHERE st.is_active = 1
             GROUP BY st.id, st.name
             ORDER BY booked_minutes DESC, booking_count DESC
             LIMIT 12",
            $date_from,
            $date_to
        ));

        $daily_breakdown = yatoon_db_get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT DATE(start_time) AS booking_date,
                    COUNT(id) AS booking_count,
                    SUM(CASE WHEN status IN ('completed','COMPLETED') THEN 1 ELSE 0 END) AS completed_count,
                    SUM(CASE WHEN status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                    SUM(total_price) AS total_revenue
             FROM " . $bookings_table . "
             WHERE DATE(start_time) BETWEEN %s AND %s
             GROUP BY DATE(start_time)
             ORDER BY booking_date DESC
            LIMIT 31",
            $date_from, $date_to
        ));

        $repeat_clients = (int) yatoon_db_get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM (
                SELECT COALESCE(NULLIF(customer_phone, ''), NULLIF(customer_email, ''), CONCAT('booking-', id)) AS client_key,
                       COUNT(id) AS booking_count
                FROM " . $bookings_table . "
                WHERE DATE(start_time) BETWEEN %s AND %s
                  AND status NOT LIKE %s
                  AND status NOT LIKE %s
                  AND status != 'DECLINED'
                GROUP BY client_key
                HAVING booking_count > 1
             ) repeat_rows",
            $date_from,
            $date_to,
            $like_cancelled_lower,
            $like_cancelled_upper
        ) );

        $now_mysql       = current_time( 'mysql' );
        $next_week_mysql = gmdate( 'Y-m-d H:i:s', strtotime( '+7 days', current_time( 'timestamp' ) ) );
        $risk_cutoff     = gmdate( 'Y-m-d H:i:s', strtotime( '-60 days', current_time( 'timestamp' ) ) );

        $upcoming_7_days = (int) yatoon_db_get_var( $wpdb->prepare(
            "SELECT COUNT(id)
             FROM " . $bookings_table . "
             WHERE start_time >= %s
               AND start_time < %s
               AND status IN ('pending','confirmed','ACCEPTED')",
            $now_mysql,
            $next_week_mysql
        ) );

        $unpaid_followups = yatoon_db_get_results(
            "SELECT b.id, b.customer_name, b.customer_phone, b.customer_email, b.start_time, b.total_price, b.payment_status,
                    s.name AS service_name
             FROM " . $bookings_table . " b
             LEFT JOIN " . $services_table . " s ON b.service_id = s.id
             WHERE (b.payment_status IS NULL OR b.payment_status = 'unpaid')
               AND b.status NOT LIKE 'cancelled%'
               AND b.status NOT LIKE 'CANCELLED%'
               AND b.status != 'DECLINED'
             ORDER BY b.start_time DESC
             LIMIT 12"
        );

        $upcoming_bookings = yatoon_db_get_results( $wpdb->prepare(
            "SELECT b.id, b.customer_name, b.customer_phone, b.customer_email, b.start_time, b.total_price,
                    s.name AS service_name, st.name AS staff_name
             FROM " . $bookings_table . " b
             LEFT JOIN " . $services_table . " s ON b.service_id = s.id
             LEFT JOIN " . $staff_table . " st ON b.staff_id = st.id
             WHERE b.start_time >= %s
               AND b.start_time < %s
               AND b.status IN ('pending','confirmed','ACCEPTED')
             ORDER BY b.start_time ASC
             LIMIT 12",
            $now_mysql,
            $next_week_mysql
        ) );

        $clients_table = esc_sql( $wpdb->prefix . 'yatoon_clients' );
        $at_risk_clients = yatoon_db_get_results( $wpdb->prepare(
            "SELECT c.customer_name, c.customer_email, c.customer_phone,
                    MAX(b.start_time) AS last_visit,
                    COUNT(b.id) AS visit_count,
                    SUM(b.total_price) AS lifetime_value
             FROM " . $clients_table . " c
             INNER JOIN " . $bookings_table . " b
                ON (b.customer_phone = c.customer_phone OR b.customer_email = c.customer_email)
             WHERE b.status NOT LIKE %s
               AND b.status NOT LIKE %s
               AND b.status != 'DECLINED'
             GROUP BY c.id
             HAVING last_visit < %s
                AND SUM(CASE WHEN b.start_time >= %s AND b.status IN ('pending','confirmed','ACCEPTED') THEN 1 ELSE 0 END) = 0
             ORDER BY last_visit ASC
             LIMIT 12",
            $like_cancelled_lower,
            $like_cancelled_upper,
            $risk_cutoff,
            $now_mysql
        ) );

        include SBS_PLUGIN_DIR . 'admin/views/reports.php';
    }
    
    /**
     * Display sync log page
     */
    public function display_sync_log_page() {
        if (!current_user_can('manage_options')) {
            return;
        }
        
        $logs = get_option('yatoon_sync_logs', array());
        $logs = array_slice(array_reverse($logs), 0, 50);
        
        include SBS_PLUGIN_DIR . 'admin/views/sync-log.php';
    }
    
    /**
     * Display contact messages page
     */
    public function display_messages_page() {
        if (!current_user_can('manage_options')) {
            return;
        }
        
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_contact_messages' );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $messages = yatoon_db_get_results("SELECT * FROM " . $table . " ORDER BY created_at DESC LIMIT 100");

        include SBS_PLUGIN_DIR . 'admin/views/messages.php';
    }

    public function display_notifications_page() {
        if ( ! current_user_can( 'manage_options' ) ) return;
        require_once YATOON_PLUGIN_DIR . 'admin/views/notifications.php';
    }

    public function display_clients_page() {
        if ( ! current_user_can( 'manage_options' ) ) return;
        require_once YATOON_PLUGIN_DIR . 'admin/views/clients.php';
    }

    public function display_waitlist_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_waitlist' );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $waitlist = yatoon_db_get_results("
            SELECT w.*, s.name as service_name, st.name as staff_name
            FROM " . $table . " w
            LEFT JOIN {$wpdb->prefix}yatoon_services s ON w.service_id = s.id
            LEFT JOIN {$wpdb->prefix}yatoon_staff st ON w.staff_id = st.id
            WHERE w.status = 'active'
            ORDER BY w.created_at DESC
        ");
        
        include SBS_PLUGIN_DIR . 'admin/views/waitlist.php';
    }

    public function display_gift_cards_page() {
        if (!current_user_can('manage_options')) return;
        global $wpdb;
        include SBS_PLUGIN_DIR . 'admin/views/gift-cards.php';
    }

    public function ajax_create_gift_card() {
        check_ajax_referer('yatoon_gift_cards', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error(array('message' => 'Permission denied'));
        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) wp_send_json_error(array('message' => 'Gift Cards'));

        global $wpdb;
        $table_gc = esc_sql( $wpdb->prefix . 'yatoon_gift_cards' );
        $table_tx = esc_sql( $wpdb->prefix . 'yatoon_gift_card_transactions' );

        $amount = max(0.01, floatval($_POST['amount'] ?? 0));
        if ($amount <= 0) wp_send_json_error(array('message' => 'Invalid amount'));

        // Generate or validate code
        $custom_code = strtoupper(preg_replace('/[^A-Z0-9-]/', '', sanitize_text_field(wp_unslash($_POST['custom_code'] ?? ''))));
        if ($custom_code) {
            $exists = yatoon_db_get_var($wpdb->prepare("SELECT id FROM " . $table_gc . " WHERE code = %s", $custom_code));
            if ($exists) wp_send_json_error(array('message' => 'This code is already in use. Please choose another.'));
            $code = $custom_code;
        } else {
            do {
                $code = strtoupper(substr(md5(uniqid(wp_rand(), true)), 0, 4))
                      . '-' . strtoupper(substr(md5(uniqid(wp_rand(), true)), 0, 4))
                      . '-' . strtoupper(substr(md5(uniqid(wp_rand(), true)), 0, 4));
            } while (yatoon_db_get_var($wpdb->prepare("SELECT id FROM " . $table_gc . " WHERE code = %s", $code)));
        }

        $expires   = sanitize_text_field(wp_unslash($_POST['expires'] ?? ''));
        $rec_name  = sanitize_text_field(wp_unslash($_POST['recipient_name'] ?? ''));
        $rec_email = sanitize_email(wp_unslash($_POST['recipient_email'] ?? ''));
        $buyer     = sanitize_text_field(wp_unslash($_POST['purchased_by'] ?? ''));
        $message   = sanitize_textarea_field(wp_unslash($_POST['message'] ?? ''));

        yatoon_db_insert($table_gc, array(
            'code'            => $code,
            'initial_amount'  => $amount,
            'balance'         => $amount,
            'purchased_by'    => $buyer,
            'purchased_email' => '',
            'recipient_name'  => $rec_name,
            'recipient_email' => $rec_email,
            'message'         => $message,
            'status'          => 'active',
            'expires_at'      => $expires ?: null,
        ));

        $gc_id = $wpdb->insert_id;

        // Record issue transaction
        yatoon_db_insert($table_tx, array(
            'gift_card_id' => $gc_id,
            'type'         => 'issue',
            'amount'       => $amount,
            'balance_after'=> $amount,
            'note'         => 'Issued by admin',
        ));

        // Send email to recipient
        if ($rec_email) {
            $this->send_gift_card_email($code, $amount, $rec_name, $rec_email, $message, $expires);
        }

        wp_send_json_success(array('code' => $code, 'id' => $gc_id));
    }

    private function send_gift_card_email($code, $amount, $rec_name, $rec_email, $message, $expires) {
        $business  = get_option('yatoon_business_name', get_bloginfo('name'));
        $primary   = get_option('yatoon_primary_color', '#C2185B');
        $first     = $rec_name ? explode(' ', $rec_name)[0] : 'there';
        $exp_line  = $expires ? '<p style="font-size:13px;color:#6b7280;">Expires: ' . gmdate('F j, Y', strtotime($expires)) . '</p>' : '';
        $msg_line  = $message ? '<p style="font-size:14px;color:#374151;font-style:italic;">"' . esc_html($message) . '"</p>' : '';

        $html = '
<div style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',sans-serif;max-width:520px;margin:0 auto;padding:32px 24px;color:#111827;">
    <h2 style="font-size:22px;font-weight:700;margin:0 0 8px;">You received a gift card!</h2>
    <p style="font-size:15px;color:#374151;margin:0 0 24px;">Hi ' . esc_html($first) . ', you\'ve received a gift card for <strong>' . esc_html($business) . '</strong>!</p>
    ' . $msg_line . '
    <div style="background:#f9fafb;border:2px dashed #e5e7eb;border-radius:12px;padding:24px;text-align:center;margin:20px 0;">
        <div style="font-size:13px;color:#9ca3af;margin-bottom:8px;">YOUR GIFT CARD CODE</div>
        <div style="font-family:monospace;font-size:28px;font-weight:700;letter-spacing:.1em;color:#111827;">' . esc_html($code) . '</div>
        <div style="font-size:22px;font-weight:700;color:' . esc_attr($primary) . ';margin-top:8px;">$' . number_format($amount, 2) . '</div>
        ' . $exp_line . '
    </div>
    <p style="font-size:14px;color:#6b7280;">Enter this code at checkout when booking your appointment online.</p>
    <p style="font-size:12px;color:#9ca3af;border-top:1px solid #f3f4f6;padding-top:16px;margin-top:24px;">This gift card was issued by ' . esc_html($business) . '.</p>
</div>';

        wp_mail($rec_email, 'You have a gift card for ' . $business, $html, array('Content-Type: text/html; charset=UTF-8'));
    }

    public function ajax_void_gift_card() {
        check_ajax_referer('yatoon_gift_cards', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) wp_send_json_error(array('message' => 'Gift Cards'));
        global $wpdb;
        yatoon_db_update($wpdb->prefix . 'yatoon_gift_cards', array('status' => 'void'), array('id' => absint($_POST['id'] ?? 0)));
        wp_send_json_success();
    }

    public function ajax_get_gift_card_history() {
        check_ajax_referer('yatoon_gift_cards', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) wp_send_json_error(array('message' => 'Gift Cards'));
        global $wpdb;
        $id  = absint($_POST['id'] ?? 0);
        $txs = yatoon_db_get_results($wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}yatoon_gift_card_transactions WHERE gift_card_id = %d ORDER BY created_at DESC",
            $id
        ));
        wp_send_json_success(array('transactions' => $txs));
    }

    public function ajax_validate_gift_card() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');
        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) wp_send_json_error(array('message' => 'Gift Cards'));
        global $wpdb;
        $code  = strtoupper(sanitize_text_field(wp_unslash($_POST['code'] ?? '')));
        $table = esc_sql( $wpdb->prefix . 'yatoon_gift_cards' );
        $gc    = yatoon_db_get_row($wpdb->prepare("SELECT * FROM " . $table . " WHERE code = %s LIMIT 1", $code));

        if (!$gc) wp_send_json_error(array('message' => 'Gift card not found.'));
        if ($gc->status !== 'active') wp_send_json_error(array('message' => 'This gift card has been ' . $gc->status . '.'));
        if ($gc->expires_at && strtotime($gc->expires_at) < time()) wp_send_json_error(array('message' => 'This gift card has expired.'));
        if (floatval($gc->balance) <= 0) wp_send_json_error(array('message' => 'This gift card has no remaining balance.'));

        wp_send_json_success(array(
            'balance' => floatval($gc->balance),
            'code'    => $gc->code,
            'message' => 'Gift card valid! Balance: $' . number_format($gc->balance, 2),
        ));
    }

    public function display_form_fields_page() {
        if (!current_user_can('manage_options')) return;
        global $wpdb;
        $fields = yatoon_db_get_results("SELECT * FROM {$wpdb->prefix}yatoon_form_fields ORDER BY sort_order ASC, id ASC");
        include SBS_PLUGIN_DIR . 'admin/views/form-fields.php';
    }

    public function display_packages_page() {
        if (!current_user_can('manage_options')) return;
        $is_pro = class_exists('YATOON_License') && YATOON_License::is_pro();
        if (!$is_pro) {
            self::render_pro_gate('Service Packages', 'Sell pre-paid session bundles (e.g. Buy 5 manicures, get 1 free). Clients redeem sessions at booking. Requires an active Pro license.');
            return;
        }
        global $wpdb;
        $packages  = yatoon_db_get_results("SELECT p.*, s.name AS service_name FROM {$wpdb->prefix}yatoon_packages p LEFT JOIN {$wpdb->prefix}yatoon_services s ON s.id = p.service_id ORDER BY p.created_at DESC");
        $services  = yatoon_db_get_results("SELECT id, name FROM {$wpdb->prefix}yatoon_services WHERE is_active = 1 ORDER BY sort_order ASC");
        include SBS_PLUGIN_DIR . 'admin/views/packages.php';
    }

    // -- Form fields AJAX ------------------------------------------------------

    public function ajax_save_form_field() {
        check_ajax_referer('yatoon_form_fields', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_form_fields' );
        $id    = absint($_POST['id'] ?? 0);
        $label = sanitize_text_field(wp_unslash($_POST['label'] ?? ''));
        if (!$label) wp_send_json_error(array('message' => 'Label required'));
        $type    = sanitize_key($_POST['type'] ?? 'text');
        $allowed = array('text','textarea','select','checkbox','radio','url');
        if (!in_array($type, $allowed, true)) $type = 'text';
        $field_key   = 'cf_' . sanitize_key(preg_replace('/[^a-z0-9]+/', '_', strtolower($label)));
        $placeholder = sanitize_text_field(wp_unslash($_POST['placeholder'] ?? ''));
        $options     = sanitize_textarea_field(wp_unslash($_POST['options'] ?? ''));
        $required    = intval($_POST['required'] ?? 0);
        if ($id > 0) {
            yatoon_db_update($table, compact('label','type','field_key','placeholder','options','required'), array('id' => $id));
        } else {
            $max = intval(yatoon_db_get_var("SELECT MAX(sort_order) FROM " . $table . ""));
            yatoon_db_insert($table, array('label'=>$label,'type'=>$type,'field_key'=>$field_key,'placeholder'=>$placeholder,'options'=>$options,'required'=>$required,'sort_order'=>$max+1,'is_active'=>1));
        }
        wp_send_json_success();
    }

    public function ajax_delete_form_field() {
        check_ajax_referer('yatoon_form_fields', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        global $wpdb;
        yatoon_db_delete($wpdb->prefix . 'yatoon_form_fields', array('id' => absint($_POST['id'] ?? 0)));
        wp_send_json_success();
    }

    public function ajax_toggle_form_field() {
        check_ajax_referer('yatoon_form_fields', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        global $wpdb;
        yatoon_db_update($wpdb->prefix . 'yatoon_form_fields', array('is_active' => intval($_POST['is_active'] ?? 0)), array('id' => absint($_POST['id'] ?? 0)));
        wp_send_json_success();
    }

    public function ajax_reorder_form_fields() {
        check_ajax_referer('yatoon_form_fields', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_form_fields' );
        $ids   = array_map('absint', explode(',', sanitize_text_field(wp_unslash($_POST['order'] ?? ''))));
        foreach ($ids as $i => $id) {
            if ($id) yatoon_db_update($table, array('sort_order' => $i), array('id' => $id));
        }
        wp_send_json_success();
    }

    // -- Packages AJAX ---------------------------------------------------------

    public function ajax_save_package() {
        check_ajax_referer('yatoon_packages', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        if (!(class_exists('YATOON_License') && YATOON_License::is_pro())) wp_send_json_error(array('message' => 'Pro required'));
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_packages' );
        $id    = absint($_POST['id'] ?? 0);
        $data  = array(
            'name'           => sanitize_text_field(wp_unslash($_POST['name'] ?? '')),
            'description'    => sanitize_textarea_field(wp_unslash($_POST['description'] ?? '')),
            'service_id'     => absint($_POST['service_id'] ?? 0),
            'sessions_total' => max(2, intval($_POST['sessions_total'] ?? 5)),
            'price'          => max(0, floatval($_POST['price'] ?? 0)),
            'expiry_days'    => max(30, intval($_POST['expiry_days'] ?? 180)),
            'is_active'      => 1,
        );
        if (!$data['name']) wp_send_json_error(array('message' => 'Name required'));
        if ($id > 0) { yatoon_db_update($table, $data, array('id' => $id)); }
        else { yatoon_db_insert($table, $data); }
        wp_send_json_success();
    }

    public function ajax_delete_package() {
        check_ajax_referer('yatoon_packages', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        global $wpdb;
        yatoon_db_delete($wpdb->prefix . 'yatoon_packages', array('id' => absint($_POST['id'] ?? 0)));
        wp_send_json_success();
    }

    public function ajax_toggle_package() {
        check_ajax_referer('yatoon_packages', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();
        global $wpdb;
        yatoon_db_update($wpdb->prefix . 'yatoon_packages', array('is_active' => intval($_POST['is_active'] ?? 0)), array('id' => absint($_POST['id'] ?? 0)));
        wp_send_json_success();
    }

    public function display_catalog_settings_page() {
        if (!current_user_can('manage_options')) return;
        if (isset($_POST['yatoon_catalog_save']) && check_admin_referer('yatoon_catalog_settings')) {
            update_option('yatoon_catalog_cols',          sanitize_text_field($_POST['catalog_cols'] ?? '3'));
            update_option('yatoon_catalog_show_price',    isset($_POST['show_price']) ? '1' : '0');
            update_option('yatoon_catalog_show_duration', isset($_POST['show_duration']) ? '1' : '0');
            update_option('yatoon_catalog_show_desc',     isset($_POST['show_desc']) ? '1' : '0');
            update_option('yatoon_catalog_show_tabs',     isset($_POST['show_tabs']) ? '1' : '0');
            update_option('yatoon_catalog_book_label',    sanitize_text_field($_POST['book_label'] ?? 'Book Now'));
            update_option('yatoon_catalog_popup',         isset($_POST['catalog_popup']) ? '1' : '0');
            // Gallery settings
            update_option('yatoon_gallery_mode',       sanitize_text_field($_POST['gallery_mode'] ?? 'grid'));
            update_option('yatoon_gallery_cols',       max(2, min(8, intval($_POST['gallery_cols'] ?? 5))));
            update_option('yatoon_gallery_gap',        max(0, min(24, intval($_POST['gallery_gap'] ?? 6))));
            update_option('yatoon_gallery_ratio',      sanitize_text_field($_POST['gallery_ratio'] ?? '1/1'));
            update_option('yatoon_gallery_lightbox',   isset($_POST['gallery_lightbox']) ? '1' : '0');
            update_option('yatoon_gallery_height',     sanitize_text_field($_POST['gallery_height'] ?? '460px'));
            update_option('yatoon_gallery_autoplay',   isset($_POST['gallery_autoplay']) ? '1' : '0');
            update_option('yatoon_gallery_interval',   max(1000, intval($_POST['gallery_interval'] ?? 3500)));
            update_option('yatoon_gallery_captions',   isset($_POST['gallery_captions']) ? '1' : '0');
            update_option('yatoon_gallery_book_label', sanitize_text_field($_POST['gallery_book_label'] ?? 'Book Now'));
            echo '<div class="notice notice-success is-dismissible"><p>Saved!</p></div>';
        }
        include SBS_PLUGIN_DIR . 'admin/views/catalog-settings.php';
    }

    public function display_reviews_page() {
        if (!current_user_can('manage_options')) return;
        include SBS_PLUGIN_DIR . 'admin/views/reviews.php';
    }

    public function display_recurring_page() {
        if (!current_user_can('manage_options')) return;
        include SBS_PLUGIN_DIR . 'admin/views/recurring.php';
    }

    public function display_closed_dates_page() {
        if (!current_user_can('manage_options')) return;

        $saved_msg = '';
        if (isset($_POST['yatoon_closed_dates_save']) && check_admin_referer('yatoon_closed_dates_save')) {
            $raw_dates = isset($_POST['yatoon_closed_dates']) ? sanitize_textarea_field(wp_unslash($_POST['yatoon_closed_dates'])) : '';
            $label     = isset($_POST['yatoon_closed_label']) ? sanitize_text_field(wp_unslash($_POST['yatoon_closed_label'])) : '';
            update_option('yatoon_closed_dates', $raw_dates);
            update_option('yatoon_closed_label', $label);
            $saved_msg = 'Saved!';
        }

        $closed_dates = get_option('yatoon_closed_dates', '');
        $closed_label = get_option('yatoon_closed_label', 'We are closed on this date.');
        include SBS_PLUGIN_DIR . 'admin/views/closed-dates.php';
    }

    public function display_blacklist_page() {
        if (!current_user_can('manage_options')) return;

        // Save if form submitted
        if (isset($_POST['yatoon_blacklist_save']) && check_admin_referer('yatoon_blacklist_save')) {
            update_option('yatoon_blacklist_phones', sanitize_textarea_field($_POST['yatoon_blacklist_phones'] ?? ''));
            update_option('yatoon_blacklist_emails', sanitize_textarea_field($_POST['yatoon_blacklist_emails'] ?? ''));
            update_option('yatoon_blacklist_ips',    sanitize_textarea_field($_POST['yatoon_blacklist_ips']    ?? ''));
            update_option('yatoon_max_future_bookings_per_phone', max(0, intval($_POST['yatoon_max_future_bookings'] ?? 0)));
            update_option('yatoon_rate_limit_per_ip',             max(1, intval($_POST['yatoon_rate_limit_per_ip']    ?? 10)));
            update_option('yatoon_rate_limit_minutes',            max(1, intval($_POST['yatoon_rate_limit_minutes']   ?? 10)));
            update_option('yatoon_min_submit_seconds',            max(0, intval($_POST['yatoon_min_submit_seconds']   ?? 3)));
            update_option('yatoon_turnstile_enabled', isset($_POST['yatoon_turnstile_enabled']) ? '1' : '0');
            update_option('yatoon_turnstile_fail_closed', isset($_POST['yatoon_turnstile_fail_closed']) ? '1' : '0');
            update_option('yatoon_turnstile_site_key', sanitize_text_field($_POST['yatoon_turnstile_site_key'] ?? ''));
            if (!empty($_POST['yatoon_turnstile_secret_key'])) {
                update_option('yatoon_turnstile_secret_key', sanitize_text_field($_POST['yatoon_turnstile_secret_key']));
            }
            update_option('yatoon_phone_lookup_enabled', isset($_POST['yatoon_phone_lookup_enabled']) ? '1' : '0');
            update_option('yatoon_phone_lookup_block_voip', isset($_POST['yatoon_phone_lookup_block_voip']) ? '1' : '0');
            echo '<div class="notice notice-success"><p>OK Blacklist & protection settings saved.</p></div>';
        }

        $phones     = get_option('yatoon_blacklist_phones', '');
        $emails     = get_option('yatoon_blacklist_emails', '');
        $ips        = get_option('yatoon_blacklist_ips', '');
        $max_future    = intval(get_option('yatoon_max_future_bookings_per_phone', 0));
        $rate_limit    = intval(get_option('yatoon_rate_limit_per_ip',             10));
        $rate_minutes  = intval(get_option('yatoon_rate_limit_minutes',            10));
        $min_submit_seconds  = intval(get_option('yatoon_min_submit_seconds', 3));
        $turnstile_enabled   = get_option('yatoon_turnstile_enabled', '0') === '1';
        $turnstile_fail_closed = get_option('yatoon_turnstile_fail_closed', '0') === '1';
        $turnstile_site_key  = get_option('yatoon_turnstile_site_key', '');
        $phone_lookup_enabled    = get_option('yatoon_phone_lookup_enabled', '0') === '1';
        $phone_lookup_block_voip = get_option('yatoon_phone_lookup_block_voip', '0') === '1';
        $twilio_ready = !empty(get_option('yatoon_twilio_account_sid')) && !empty(YATOON_Crypto::get_option('yatoon_twilio_auth_token'));
        ?>
        <div class="wrap">
            <h1>Booking Protection &amp; Blacklist</h1>
            <p style="color:#555;max-width:700px;">Block abusive customers and limit excessive bookings. All checks run silently - blocked visitors see a generic "please call us" message.</p>

            <form method="post">
                <?php wp_nonce_field('yatoon_blacklist_save'); ?>
                <input type="hidden" name="yatoon_blacklist_save" value="1">

                <table class="form-table" style="max-width:800px;">
                    <tr>
                        <th scope="row" style="width:220px;">
                            <label>Max upcoming bookings<br><small style="font-weight:normal;color:#777;">per phone number</small></label>
                        </th>
                        <td>
                            <input type="number" name="yatoon_max_future_bookings" value="<?php echo esc_attr($max_future); ?>" min="0" max="50" style="width:80px;">
                            <p class="description">Set to <strong>0 to disable</strong> this check (recommended - Square cancellations may not sync instantly). Set to a number (e.g. 5) to block a phone number from having more than that many upcoming appointments.</p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">
                            <label>IP rate limit<br><small style="font-weight:normal;color:#777;">max attempts per window</small></label>
                        </th>
                        <td>
                            <input type="number" name="yatoon_rate_limit_per_ip" value="<?php echo esc_attr($rate_limit); ?>" min="1" max="50" style="width:80px;">
                            <span style="margin-left:10px;color:#555;">attempts within</span>
                            <input type="number" name="yatoon_rate_limit_minutes" value="<?php echo esc_attr($rate_minutes); ?>" min="1" max="60" style="width:70px;margin-left:8px;">
                            <span style="margin-left:6px;color:#555;">minutes</span>
                            <p class="description">Block the same IP after this many submissions in the time window. Default: <strong>10 attempts / 10 minutes</strong>. Prevents bots without inconveniencing real customers.</p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label>Blocked phone numbers</label></th>
                        <td>
                            <textarea name="yatoon_blacklist_phones" rows="6" style="width:400px;font-family:monospace;"><?php echo esc_textarea($phones); ?></textarea>
                            <p class="description">One number per line. Any format: <code>3105550000</code>, <code>(310) 555-0000</code>, <code>+1-310-555-0000</code></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label>Blocked email addresses</label></th>
                        <td>
                            <textarea name="yatoon_blacklist_emails" rows="6" style="width:400px;font-family:monospace;"><?php echo esc_textarea($emails); ?></textarea>
                            <p class="description">One email per line. Case-insensitive.</p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label>Blocked IP addresses</label></th>
                        <td>
                            <textarea name="yatoon_blacklist_ips" rows="4" style="width:400px;font-family:monospace;"><?php echo esc_textarea($ips); ?></textarea>
                            <p class="description">One IP per line (IPv4). Use sparingly - IPs can change. Best for repeated bot attacks.</p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label>Minimum time to submit</label></th>
                        <td>
                            <input type="number" name="yatoon_min_submit_seconds" value="<?php echo esc_attr($min_submit_seconds); ?>" min="0" max="30" style="width:70px;"> seconds
                            <p class="description">Silently rejects submissions that arrive faster than a real person could fill the form - scripts that fetch the page and POST immediately get caught here. Default: <strong>3 seconds</strong>. Set to 0 to disable.</p>
                        </td>
                    </tr>
                </table>

                <h2 style="margin-top:28px;">Invisible bot check (Cloudflare Turnstile)</h2>
                <p style="color:#555;max-width:700px;">Adds no extra step for real customers - it runs in the background while they fill out the form. Free, and separate from the Twilio/OTP settings above. <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank">Get free keys from Cloudflare</a>.</p>
                <table class="form-table" style="max-width:800px;">
                    <tr>
                        <th scope="row">Enable</th>
                        <td><label><input type="checkbox" name="yatoon_turnstile_enabled" value="1" <?php checked($turnstile_enabled, true); ?>> Require an invisible Turnstile check before a booking is accepted</label></td>
                    </tr>
                    <tr>
                        <th scope="row">Strict mode</th>
                        <td>
                            <label><input type="checkbox" name="yatoon_turnstile_fail_closed" value="1" <?php checked($turnstile_fail_closed, true); ?>> Block bookings when Turnstile cannot return or verify a token</label>
                            <p class="description">Default is off so real customers are not blocked by browser extensions, script blockers, or a temporary Cloudflare/network issue. Turn this on during active bot attacks.</p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="yatoon_turnstile_site_key">Site Key</label></th>
                        <td><input type="text" id="yatoon_turnstile_site_key" name="yatoon_turnstile_site_key" value="<?php echo esc_attr($turnstile_site_key); ?>" class="regular-text"></td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="yatoon_turnstile_secret_key">Secret Key</label></th>
                        <td>
                            <input type="password" name="yatoon_turnstile_secret_key" id="yatoon_turnstile_secret_key" value="" class="regular-text" autocomplete="new-password" placeholder="<?php echo esc_attr( yatoon_secret_placeholder( 'yatoon_turnstile_secret_key' ) ); ?>">
                            <p class="description"><?php echo esc_html( yatoon_secret_saved_note( 'yatoon_turnstile_secret_key' ) ); ?></p>
                        </td>
                    </tr>
                </table>

                <h2 style="margin-top:28px;">Automatic phone number verification</h2>
                <p style="color:#555;max-width:700px;">
                    Checks that the phone number is real and dialable the moment a customer submits - no code to type, no extra step.
                    <?php if ($twilio_ready): ?>
                        Uses the Twilio credentials already configured in the <strong>SMS</strong> tab.
                    <?php else: ?>
                        <strong>Requires Twilio Account SID + Auth Token to be set in the SMS tab first</strong> (this reuses those credentials via Twilio's Lookup API).
                    <?php endif; ?>
                </p>
                <table class="form-table" style="max-width:800px;">
                    <tr>
                        <th scope="row">Enable</th>
                        <td><label><input type="checkbox" name="yatoon_phone_lookup_enabled" value="1" <?php checked($phone_lookup_enabled, true); disabled(!$twilio_ready); ?>> Reject bookings with an invalid or non-existent phone number</label></td>
                    </tr>
                    <tr>
                        <th scope="row">Block VOIP numbers</th>
                        <td>
                            <label><input type="checkbox" name="yatoon_phone_lookup_block_voip" value="1" <?php checked($phone_lookup_block_voip, true); disabled(!$twilio_ready); ?>> Also reject burner/VOIP numbers (e.g. Google Voice, TextNow)</label>
                            <p class="description">Optional - this line-type check carries a small per-lookup fee from Twilio (a fraction of a cent). Leave off if you just want basic validity checking, which is free.</p>
                        </td>
                    </tr>
                </table>


                <p style="margin-top:20px;">
                    <button type="submit" class="button button-primary button-large">Save Protection Settings</button>
                </p>
            </form>

            <hr style="margin:40px 0 24px;">
            <h2 style="font-size:15px;">How each layer works</h2>
            <table style="border-collapse:collapse;max-width:680px;font-size:13px;">
                <tr style="background:#f6f7f7;">
                    <td style="padding:10px 14px;border:1px solid #ddd;font-weight:600;white-space:nowrap;">Phone format</td>
                    <td style="padding:10px 14px;border:1px solid #ddd;">Must be a valid 10-digit US number. Rejects obviously fake inputs immediately.</td>
                </tr>
                <tr>
                    <td style="padding:10px 14px;border:1px solid #ddd;font-weight:600;white-space:nowrap;">Blacklist</td>
                    <td style="padding:10px 14px;border:1px solid #ddd;">Phone, email, and IP are checked against your lists before any booking is created.</td>
                </tr>
                <tr style="background:#f6f7f7;">
                    <td style="padding:10px 14px;border:1px solid #ddd;font-weight:600;white-space:nowrap;">IP rate limit</td>
                    <td style="padding:10px 14px;border:1px solid #ddd;">Blocks rapid-fire submissions from the same connection. Auto-resets after 30 minutes.</td>
                </tr>
                <tr>
                    <td style="padding:10px 14px;border:1px solid #ddd;font-weight:600;white-space:nowrap;">Future booking cap</td>
                    <td style="padding:10px 14px;border:1px solid #ddd;">One phone number cannot hold more than your set limit of upcoming appointments at the same time.</td>
                </tr>
            </table>
        </div>
        <?php
    }

    
    public function display_deposit_pro_page() {
        if (!current_user_can('manage_options')) return;
        if (function_exists('ybs_is_pro') && ybs_is_pro()) {
            $_GET['tab'] = 'deposit';
            include SBS_PLUGIN_DIR . 'admin/views/settings.php';
            return;
        }
        self::render_pro_gate(
            'Deposit & Prepay',
            'Require customers to pay a deposit or full prepayment at booking time. Powered by Stripe. Requires an active Pro license.'
        );
    }

    public function display_noshow_pro_page() {
        if (!current_user_can('manage_options')) return;
        if (function_exists('ybs_is_pro') && ybs_is_pro()) {
            include SBS_PLUGIN_DIR . 'admin/views/settings.php';
            return;
        }
        self::render_pro_gate(
            'No-show Tracking & Auto-deposit',
            'Automatically flag clients with 3+ no-shows and require a deposit on future bookings. Requires an active Pro license.'
        );
    }

    public function display_waitlist_pro_page() {
        if (!current_user_can('manage_options')) return;
        if (function_exists('ybs_is_pro') && ybs_is_pro()) {
            include SBS_PLUGIN_DIR . 'admin/views/waitlist.php';
            return;
        }
        self::render_pro_gate(
            'Waitlist Auto-notify',
            'When a cancellation opens up, the plugin automatically emails the next person on the waitlist - no manual work. Requires an active Pro license.'
        );
    }

    public function display_brand_page() {
        if (!current_user_can('manage_options')) return;
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            self::render_pro_gate('Brand & Colors', 'Custom branding, logo styling, and advanced visual controls stay in Pro.');
            return;
        }

        include SBS_PLUGIN_DIR . 'admin/views/brand-settings.php';
    }

    public function display_sms_pro_page() {
        if (!current_user_can('manage_options')) return;
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            self::render_pro_gate('SMS Notifications', 'SMS reminders and Twilio delivery require an active Pro license.');
            return;
        }

        // PRO: show SMS settings tab directly
        $_GET['tab'] = 'sms'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- internal tab override
        include SBS_PLUGIN_DIR . 'admin/views/settings.php';
    }

    public function display_gcal_pro_page() {
        if (!current_user_can('manage_options')) return;
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            self::render_pro_gate('Google Calendar', 'Google Calendar sync requires an active Pro license.');
            return;
        }

        $_GET['tab'] = 'calendar'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- internal tab override
        include SBS_PLUGIN_DIR . 'admin/views/settings.php';
    }

    public function display_staff_crud_pro_page() {
        if (!current_user_can('manage_options')) return;
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            self::render_pro_gate('Manual Staff Management', 'Advanced staff controls require an active Pro license.');
            return;
        }

        $_GET['tab'] = 'staff_crud'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- internal tab override
        include SBS_PLUGIN_DIR . 'admin/views/settings.php';
    }

    public function display_license_page() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        wp_safe_redirect( YBS_UPGRADE_URL );
        exit;
    }

    /**
     * Handle Business Hours form save
     */
    public function handle_save_business_hours() {
        if (!current_user_can('manage_options')) wp_die('Permission denied');
        check_admin_referer('yatoon_save_business_hours');

        $hours_raw = isset($_POST['hours']) ? $_POST['hours'] : array();
        YATOON_Business_Hours::save_hours($hours_raw);

        $interval = intval($_POST['sbs_slot_interval'] ?? 30);
        update_option('yatoon_slot_interval', in_array($interval, array(15,30,45,60)) ? $interval : 30);

        // Save multi-appointment mode toggle
        $multi_appt = isset($_POST['yatoon_multi_appointment']) ? '1' : '0';
        update_option('yatoon_multi_appointment', $multi_appt);

        // Save booking app mode
        $immersive_mode = isset($_POST['yatoon_booking_immersive_mode']) ? '1' : '0';
        update_option('yatoon_booking_immersive_mode', $immersive_mode);

        // Save guest-together limit
        $max_guests_together = intval($_POST['yatoon_max_guests_together'] ?? 2);
        update_option('yatoon_max_guests_together', max(0, min(10, $max_guests_together)));

        // Save latest end hour
        $latest_end_hour = intval($_POST['yatoon_latest_end_hour'] ?? 20);
        update_option('yatoon_latest_end_hour', in_array($latest_end_hour, array(17,18,19,20,21,22)) ? $latest_end_hour : 20);

        // Save scroll offset
        $header_offset = intval($_POST['sbs_header_offset'] ?? 0);
        update_option('yatoon_header_offset', max(0, min(300, $header_offset)));

        // Save auto-confirm bookings toggle
        $auto_confirm = isset($_POST['yatoon_auto_confirm_bookings']) ? '1' : '0';
        update_option('yatoon_auto_confirm_bookings', $auto_confirm);

        wp_safe_redirect(admin_url('admin.php?page=yatoon-booking-system&tab=hours&saved=1'));
        exit;
    }

    /**
     * Render PRO upgrade gate page - professional upsell wall
     */
    public static function render_pro_gate( $feature_name, $feature_desc = '' ) {
        $upgrade_url = defined( 'YBS_UPGRADE_URL' ) ? YBS_UPGRADE_URL : 'https://checkout.freemius.com/plugin/24668/plan/40958/';
        ?>
        <div class="wrap">
            <div style="max-width:620px; margin:60px auto; text-align:center; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">

                <div style="display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:6px 14px;margin:0 auto 22px;border-radius:999px;background:#111827;color:#fff;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;">
                    Yatoon Pro
                </div>

                <!-- Title -->
                <h1 style="font-size:26px;font-weight:800;color:#111;margin:0 0 12px;">
                    <?php echo esc_html($feature_name); ?> helps growing salons run fewer disconnected tools
                </h1>

                <?php if ($feature_desc) : ?>
                <p style="font-size:16px;color:#6b7280;line-height:1.6;margin:0 0 32px;">
                    <?php echo esc_html($feature_desc); ?>
                </p>
                <?php endif; ?>

                <!-- Feature comparison -->
                <div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:16px;padding:28px;text-align:left;margin-bottom:32px;">
                    <div style="display:grid;grid-template-columns:1fr 1fr;gap:0;border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;">
                        <div style="background:#fff;padding:16px;border-right:1px solid #e5e7eb;">
                            <p style="margin:0 0 12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;">FREE</p>
                            <?php
                            $free_items = [
                                'Yes Local WordPress booking',
                                'Yes Customer booking page',
                                'Yes Email confirmations',
                                'Yes Services, staff, and hours',
                                'Yes Client records and notes',
                                'Yes Staff portal schedule view',
                                'No Square sync',
                                'No Square online payments',
                                'No SMS reminders',
                                'No Deposits and no-show automation',
                            ];
                            foreach ($free_items as $item) {
                                $color = strpos($item, 'Yes') === 0 ? '#374151' : '#d1d5db';
                                echo '<p style="margin:0 0 6px;font-size:13px;color:'.$color.';">' . esc_html($item) . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                            }
                            ?>
                        </div>
                        <div style="background:linear-gradient(160deg,#fdf2f8,#f5f3ff);padding:16px;">
                            <p style="margin:0 0 12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#ec4899;">PRO *</p>
                            <?php
                            $pro_items = [
                                'Yes Everything in Free',
                                'Yes Square Appointments sync',
                                'Yes Square online payments',
                                'Yes Deposits and prepay',
                                'Yes SMS via Twilio',
                                'Yes Google Calendar sync',
                                'Yes Packages and memberships',
                                'Yes No-show workflows',
                                'Yes Advanced brand controls',
                                'Yes Priority support',
                            ];
                            foreach ($pro_items as $item) {
                                echo '<p style="margin:0 0 6px;font-size:13px;color:#374151;font-weight:500;">' . esc_html($item) . '</p>';
                            }
                            ?>
                        </div>
                    </div>
                </div>

                <!-- CTA -->
                <a href="<?php echo esc_url($upgrade_url); ?>"
                   target="_blank"
                   rel="noopener noreferrer"
                   style="display:inline-block;padding:14px 40px;background:linear-gradient(135deg,#ec4899,#a855f7);color:#fff;font-size:16px;font-weight:700;border-radius:50px;text-decoration:none;box-shadow:0 4px 16px rgba(236,72,153,.4);transition:all .2s;">
                    See Pro Pricing
                </a>
                <p style="margin:16px 0 0;font-size:13px;color:#9ca3af;">
                    Use Free for local booking. Upgrade when Square, payments, automation, and staff growth tools matter.
                </p>

            </div>
        </div>
        <?php
    }

    /**
     * AJAX: test connection
     */
    public function ajax_test_connection() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied', 'yatoon-booking-system')));
        }
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            wp_send_json_error(array('message' => 'Square sync requires an active Pro license.'));
        }
        
        $api = new YATOON_Square_API();
        $result = $api->test_connection();
        
        if (is_array($result) && isset($result['success'])) {
            if ($result['success']) {
                wp_send_json_success(array(
                    'message' => $result['message'],
                    'locations' => isset($result['locations']) ? $result['locations'] : array()
                ));
            } else {
                wp_send_json_error(array(
                    'message' => isset($result['error']) ? $result['error'] : __('Connection failed', 'yatoon-booking-system'),
                    'details' => isset($result['error_code']) ? $result['error_code'] : ''
                ));
            }
        } else {
            wp_send_json_error(array('message' => __('Connection test failed. Please check your credentials.', 'yatoon-booking-system')));
        }
    }
    
    /**
     * AJAX: sync now
     */
    /**
     * AJAX: Test Vagaro connection
     */
    public function ajax_test_vagaro_connection() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if ( ! current_user_can('manage_options') ) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            wp_send_json_error(array('message' => 'Vagaro sync requires an active Pro license.'));
        }
        $api    = new YATOON_Vagaro_API();
        $result = $api->test_connection();
        if ( is_wp_error($result) ) {
            wp_send_json_error(array('message' => $result->get_error_message()));
        }
        wp_send_json_success($result);
    }

    public function ajax_sync_now() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied', 'yatoon-booking-system')));
        }

        // Extend execution time - Square API can be slow
        @set_time_limit(300);
        @ignore_user_abort(true);

        try {
            $cron = new YATOON_Cron();
            $result = $cron->sync_square_data( true );

            if ($result) {
                update_option('yatoon_last_sync', time());
                wp_send_json_success(array(
                    'message' => __('Sync completed successfully!', 'yatoon-booking-system'),
                    'data' => $result
                ));
            } else {
                wp_send_json_error(array('message' => __('Sync failed. Check the Sync Log tab for details.', 'yatoon-booking-system')));
            }
        } catch ( \Throwable $e ) {
            yatoon_safe_log('SBS ajax_sync_now fatal: ' . $e->getMessage());
            wp_send_json_error(array('message' => 'Sync error: ' . $e->getMessage()));
        }
    }
    
    /**
     * AJAX: delete service
     */
    public function ajax_delete_service() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => __('Permission denied', 'yatoon-booking-system')));
        }
        
        $service_ids = array();
        if (isset($_POST['service_ids']) && is_array($_POST['service_ids'])) {
            $service_ids = array_map('intval', wp_unslash($_POST['service_ids']));
        } elseif (isset($_POST['service_id'])) {
            $service_ids = array(intval($_POST['service_id']));
        }

        $service_ids = array_values(array_unique(array_filter($service_ids)));

        if (empty($service_ids)) {
            wp_send_json_error(array('message' => __('Invalid service ID', 'yatoon-booking-system')));
        }
        
        global $wpdb;
        $services_table       = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $options_table        = esc_sql( $wpdb->prefix . 'yatoon_service_options' );
        $staff_services_table = esc_sql( $wpdb->prefix . 'yatoon_staff_services' );
        $deleted              = 0;

        foreach ($service_ids as $service_id) {
            // Remove related configuration first so deleted services do not leave orphan rows.
            yatoon_db_delete($options_table, array('service_id' => $service_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            yatoon_db_delete($staff_services_table, array('service_id' => $service_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

            $result = yatoon_db_delete($services_table, array('id' => $service_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            if ($result) {
                $deleted++;
            }
        }

        if ($deleted > 0) {
            wp_send_json_success(array(
                'message' => sprintf(
                    /* translators: %d: number of deleted services */
                    _n('%d service deleted successfully', '%d services deleted successfully', $deleted, 'yatoon-booking-system'),
                    $deleted
                ),
                'deleted' => $deleted,
            ));
        } else {
            wp_send_json_error(array('message' => __('Failed to delete service', 'yatoon-booking-system')));
        }
    }
    
    /**
     * AJAX: Save general settings - bypasses options.php redirect issues
     */
    public function ajax_create_staff_portal_page() {
        check_ajax_referer( 'yatoon_create_staff_portal_page', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => 'Permission denied' ) );
        }

        global $wpdb;
        $like = '%' . $wpdb->esc_like( '[yatoon_staff_portal' ) . '%';
        $existing_id = (int) yatoon_db_get_var( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status IN ('publish','draft','private') AND post_content LIKE %s ORDER BY ID DESC LIMIT 1",
            $like
        ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery

        if ( $existing_id > 0 ) {
            update_option( 'yatoon_staff_portal_page_id', $existing_id );
            wp_send_json_success( array(
                'message'  => 'Staff Portal page already exists.',
                'page_id'  => $existing_id,
                'view_url' => get_permalink( $existing_id ),
                'edit_url' => get_edit_post_link( $existing_id, '' ),
            ) );
        }

        $page_id = wp_insert_post( array(
            'post_title'   => 'Staff Portal',
            'post_name'    => 'staff-portal',
            'post_status'  => 'publish',
            'post_type'    => 'page',
            'post_content' => "[yatoon_staff_portal]\n\n<!-- Staff sign in page for Yatoon Booking. Share this page with staff after setting their PIN and permissions. -->",
        ), true );

        if ( is_wp_error( $page_id ) || ! $page_id ) {
            wp_send_json_error( array( 'message' => is_wp_error( $page_id ) ? $page_id->get_error_message() : 'Could not create Staff Portal page.' ) );
        }

        update_option( 'yatoon_staff_portal_page_id', (int) $page_id );

        wp_send_json_success( array(
            'message'  => 'Staff Portal page created.',
            'page_id'  => (int) $page_id,
            'view_url' => get_permalink( $page_id ),
            'edit_url' => get_edit_post_link( $page_id, '' ),
        ) );
    }

    /**
     * AJAX: update option name and price
     */
    public function ajax_save_general_settings() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if ( ! current_user_can('manage_options') ) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }

        // Platform
        if ( isset($_POST['yatoon_active_platform']) ) {
            $platform = sanitize_key(wp_unslash($_POST['yatoon_active_platform']));
            if ( ! YATOON_License::is_pro() ) {
                $platform = 'local';
            }
            if ( in_array($platform, array('square', 'vagaro', 'local'), true) ) {
                update_option('yatoon_active_platform', $platform);
            }
        }

        // Text / color options (all fields from the general settings tab)
        $text_opts = array(
            // Square / Vagaro API
            'yatoon_booking_template', 'yatoon_square_access_token', 'yatoon_square_location_id',
            'sbs_square_environment', 'yatoon_square_environment',
            'yatoon_sync_interval', 'yatoon_booking_sync_interval',
            'yatoon_vagaro_api_key', 'yatoon_vagaro_business_id',
            // Phone / OTP
            'yatoon_phone_mode', 'yatoon_phone_preset_country', 'yatoon_otp_expiry_minutes',
            // Policy & Consent
            'yatoon_policy_title', 'yatoon_policy_text', 'yatoon_policy_ack_label',
            'yatoon_consent_title', 'yatoon_consent_text', 'yatoon_consent_signature_label',
            // Portal
            'yatoon_book_new_url', 'yatoon_portal_url', 'yatoon_portal_label',
            'yatoon_portal_banner_prompt', 'yatoon_portal_banner_bg', 'yatoon_portal_banner_text',
            'yatoon_portal_login_title', 'yatoon_portal_login_subtitle',
            'yatoon_portal_login_brand', 'yatoon_portal_btn_label', 'yatoon_portal_primary_color',
            // Venue / booking copy
            'yatoon_venue_kicker', 'yatoon_public_rating_label',
            'yatoon_venue_pay_later_label', 'yatoon_venue_deposit_label',
            'yatoon_availability_help_text', 'yatoon_latest_end_hour',
            'yatoon_success_next_title', 'yatoon_success_pay_later_text',
            'yatoon_success_deposit_text', 'yatoon_success_followup_text',
            'yatoon_success_action_note',
            'yatoon_rebooking_nudge_text', 'yatoon_rebooking_nudge_btn',
            // Tip
            'yatoon_tip_label', 'yatoon_tip_presets',
            // WhatsApp
            'yatoon_whatsapp_number', 'yatoon_whatsapp_prefill', 'yatoon_whatsapp_label',
            // Booking page colors
            'yatoon_price_color', 'yatoon_banner_bg_color', 'yatoon_banner_text_color',
            'yatoon_banner_btn_color', 'yatoon_textus_bg_color', 'yatoon_textus_text_color',
            // Deposit / Stripe
            'yatoon_deposit_type', 'yatoon_deposit_stripe_key',
            'yatoon_deposit_stripe_secret', 'yatoon_stripe_webhook_secret',
            'yatoon_square_webhook_signature_key',
            'yatoon_payment_request_url', 'yatoon_square_checkout_url',
            'yatoon_square_app_url', 'yatoon_square_checkout_note',
            'yatoon_square_no_show_fee_note',
            // Business info
            'yatoon_contact_phone', 'yatoon_contact_email',
            'yatoon_business_phone', 'yatoon_business_address', 'yatoon_business_website',
            'sbs_business_phone', 'sbs_business_address', 'sbs_business_website',
        );
        $protected_empty_keys = array(
            'yatoon_square_access_token',
            'yatoon_vagaro_api_key',
            'yatoon_deposit_stripe_secret',
            'yatoon_stripe_webhook_secret',
            'yatoon_square_webhook_signature_key',
            'yatoon_twilio_auth_token',
            'yatoon_google_cal_client_secret',
            'yatoon_google_cal_refresh_token',
        );
        foreach ( $text_opts as $opt ) {
            if ( array_key_exists($opt, $_POST) ) {
                $raw_value = wp_unslash($_POST[$opt]);
                $value = ( substr($opt, -6) === '_color' || substr($opt, -7) === '_colour' )
                    ? sanitize_hex_color($raw_value)
                    : sanitize_text_field($raw_value);
                if ( $value === '' && in_array( $opt, $protected_empty_keys, true ) ) {
                    continue;
                }
                if ( YATOON_Crypto::is_sensitive( $opt ) ) {
                    YATOON_Crypto::update_option( $opt, $value );
                } else {
                    update_option($opt, $value);
                }
            }
        }

        // Textarea fields (allow line breaks)
        foreach ( array('yatoon_policy_text', 'yatoon_consent_text') as $opt ) {
            if ( array_key_exists($opt, $_POST) ) {
                update_option($opt, sanitize_textarea_field(wp_unslash($_POST[$opt])));
            }
        }

        // Sync aliases
        if ( array_key_exists('yatoon_business_phone', $_POST) ) {
            update_option('sbs_business_phone', sanitize_text_field(wp_unslash($_POST['yatoon_business_phone'])));
        }
        if ( array_key_exists('yatoon_business_address', $_POST) ) {
            update_option('sbs_business_address', sanitize_text_field(wp_unslash($_POST['yatoon_business_address'])));
        }
        if ( array_key_exists('yatoon_business_website', $_POST) ) {
            update_option('sbs_business_website', sanitize_text_field(wp_unslash($_POST['yatoon_business_website'])));
        }

        // Numeric fields
        if ( array_key_exists('yatoon_deposit_amount', $_POST) ) {
            update_option('yatoon_deposit_amount', max(0, floatval(wp_unslash($_POST['yatoon_deposit_amount']))));
        }
        if ( array_key_exists('yatoon_square_no_show_fee_amount', $_POST) ) {
            update_option('yatoon_square_no_show_fee_amount', max(0, floatval(wp_unslash($_POST['yatoon_square_no_show_fee_amount']))));
        }
        if ( array_key_exists('yatoon_max_guests_together', $_POST) ) {
            update_option('yatoon_max_guests_together', max(0, min(10, intval(wp_unslash($_POST['yatoon_max_guests_together'])))));
        }

        // Checkboxes - ONLY fields that are actually in the General tab form.
        // Do NOT include fields from Email/SMS/Hours/Calendar tabs here; saving
        // General would otherwise force-clear those options because they are
        // absent from the POST payload.
        $checkboxes = array(
            // Sync
            'yatoon_auto_sync_enabled', 'yatoon_booking_sync_enabled',
            'yatoon_square_no_show_fee_enabled',
            // PWA (general tab)
            'yatoon_pwa_enabled', 'yatoon_show_advanced_menu', 'yatoon_test_booking_mode',
            // Phone / OTP (general tab)
            'yatoon_phone_required', 'yatoon_verify_email_otp', 'yatoon_verify_sms_otp',
            // Policy & Consent (general tab)
            'yatoon_policy_enabled', 'yatoon_policy_require_ack',
            'yatoon_consent_enabled',
            // Waitlist (general tab)
            'yatoon_waitlist_enabled', 'yatoon_waitlist_auto_notify',
            // Multi-location (general tab)
            'yatoon_multi_location_enabled',
            // Availability display (general tab)
            'yatoon_show_next_opening', 'yatoon_show_availability_source_note',
            'yatoon_allow_design_upload',
            // Success screen (general tab)
            'yatoon_show_success_next', 'yatoon_show_venue_hero',
            // Checkout (general tab)
            'yatoon_tip_enabled', 'yatoon_gift_card_enabled', 'yatoon_enable_coupons',
            // Portal banner (general tab)
            'yatoon_show_portal_banner', 'yatoon_portal_new_tab',
            // Ratings (general tab)
            'yatoon_show_ratings_on_cards',
            // Deposit (general tab)
            'yatoon_deposit_enabled',
            // WhatsApp / Text Us (general tab)
            'yatoon_whatsapp_enabled', 'yatoon_text_us_enabled',
            // AI assistant (general tab)
            'yatoon_ai_service_helper_enabled', 'yatoon_ai_reply_drafts_enabled',
        );
        foreach ( $checkboxes as $opt ) {
            update_option($opt, !empty($_POST[$opt]) ? '1' : '');
        }

        if ( class_exists('YATOON_Cron') ) YATOON_Cron::maybe_reschedule_booking_sync();

        wp_send_json_success(array('message' => 'Settings saved!'));
    }

    public function ajax_update_option() {
        check_ajax_referer('yatoon_option', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $option_id      = isset($_POST['option_id'])      ? intval($_POST['option_id'])          : 0;
        $name           = isset($_POST['name'])           ? sanitize_text_field( wp_unslash( $_POST['name'] ) )   : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $price          = isset($_POST['price'])          ? floatval($_POST['price'])             : 0;
        $group_name     = isset($_POST['group_name'])     ? sanitize_text_field( wp_unslash( $_POST['group_name'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $badge_label    = isset($_POST['badge_label'])    ? sanitize_key( wp_unslash( $_POST['badge_label'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $duration_minutes = isset($_POST['duration_minutes']) ? intval($_POST['duration_minutes']) : null;
        // JS sends price_modifier = absolute_price - base_price
        $has_modifier   = isset($_POST['price_modifier']);
        $price_modifier = $has_modifier ? floatval($_POST['price_modifier']) : null;

        if (!$option_id || empty($name) || $price < 0) {
            wp_send_json_error('Invalid data');
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_service_options' );
        $badge_columns = yatoon_db_get_col("SHOW COLUMNS FROM `" . $table . "` LIKE 'badge_label'");
        if (empty($badge_columns)) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            yatoon_db_query("ALTER TABLE `" . $table . "` ADD COLUMN badge_label varchar(50) DEFAULT '' AFTER group_name");
        }

        // If price_modifier not sent, compute it from the service base price.
        // Add-on options stack on top of whatever Main option is selected, so their
        // modifier must be the flat amount to add - not relative to the base price
        // (that formula is only correct for Main options).
        if ($price_modifier === null) {
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $row = yatoon_db_get_row($wpdb->prepare(
                "SELECT o.service_id, o.group_name, s.price as base_price
                 FROM " . $table . " o
                 JOIN {$wpdb->prefix}yatoon_services s ON s.id = o.service_id
                 WHERE o.id = %d", $option_id
            ));
            $effective_group = $group_name !== null ? $group_name : ( $row ? $row->group_name : null );
            $price_modifier  = $row
                ? ( $effective_group === 'Add-on' ? $price : ( $price - floatval($row->base_price) ) )
                : 0;
        }

        $update_data   = array('name' => $name, 'price_modifier' => $price_modifier);
        $update_format = array('%s', '%f');
        if ($group_name !== null) {
            $update_data['group_name']   = $group_name === '' ? null : $group_name;
            $update_format[]             = '%s';
        }
        if ($badge_label !== null) {
            $allowed_badges = array('', 'recommended', 'popular', 'addon', 'none');
            $update_data['badge_label'] = in_array($badge_label, $allowed_badges, true) ? $badge_label : '';
            $update_format[]            = '%s';
        }
        if ($duration_minutes !== null && $duration_minutes > 0) {
            $update_data['duration_minutes'] = $duration_minutes;
            $update_format[]                 = '%d';
        }
        $result = yatoon_db_update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $table,
            $update_data,
            array('id' => $option_id),
            $update_format,
            array('%d')
        );

        if ($result !== false) {
            wp_send_json_success(array('message' => 'Option updated successfully', 'price_modifier' => $price_modifier));
        } else {
            wp_send_json_error('Failed to update option: ' . $wpdb->last_error);
        }
    }
    
    /**
     * AJAX: get staff service list
     */
    /**
     * AJAX: get all Add-on options from a source service (for copy-as-addon feature)
     */
    /**
     * AJAX: delete a service option row
     */
    public function ajax_delete_option() {
        check_ajax_referer('yatoon_option', 'nonce');
        if (!current_user_can('manage_options')) { wp_send_json_error('Permission denied'); }

        $option_id = isset($_POST['option_id']) ? intval($_POST['option_id']) : 0;
        if (!$option_id) {
            wp_send_json_error('Invalid option_id');
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_service_options' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = yatoon_db_delete($table, array('id' => $option_id), array('%d'));
        if ($result !== false) {
            wp_send_json_success(array('message' => 'Option deleted'));
        } else {
            wp_send_json_error('Failed to delete: ' . $wpdb->last_error);
        }
    }

    /**
     * AJAX: get all options for a given service (for the Edit Service modal)
     */
    public function ajax_get_service_options() {
        check_ajax_referer('yatoon_option', 'nonce');
        if (!current_user_can('manage_options')) { wp_send_json_error('Permission denied'); }

        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
        if (!$service_id) { wp_send_json_error('Invalid service_id'); }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_service_options' );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $options = yatoon_db_get_results($wpdb->prepare(
            "SELECT * FROM " . $table . " WHERE service_id = %d ORDER BY sort_order, id",
            $service_id
        ));
        wp_send_json_success(array('options' => $options));
    }

    public function ajax_get_addon_options() {
        check_ajax_referer('yatoon_option', 'nonce');
        if (!current_user_can('manage_options')) { wp_send_json_error('Permission denied'); }
        global $wpdb;
        // Return all reusable service options for the picker. Local-only/manual options may not have a Square ID.
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $rows = yatoon_db_get_results(
            "SELECT s.id as service_id, s.name as service_name,
                    o.id as option_id, o.name as option_name,
                    o.square_id, o.square_version, o.duration_minutes, o.price_modifier, o.group_name,
                    s.price AS base_price
             FROM {$wpdb->prefix}yatoon_services s
             JOIN {$wpdb->prefix}yatoon_service_options o ON o.service_id = s.id
             ORDER BY s.name ASC, o.name ASC"
        );
        $services = yatoon_db_get_results(
            "SELECT id AS service_id, name AS service_name, square_id,
                    duration_minutes, price, allow_design_upload
             FROM {$wpdb->prefix}yatoon_services
             WHERE is_active = 1
             ORDER BY name ASC"
        );
        $items = array();
        foreach ((array) $services as $service) {
            $items[] = array(
                'key'                 => 'service:' . intval($service->service_id),
                'source_type'         => 'service',
                'source_id'           => intval($service->service_id),
                'service_id'          => intval($service->service_id),
                'group_label'         => 'Services',
                'label'               => $service->service_name,
                'name'                => $service->service_name,
                'duration_minutes'    => intval($service->duration_minutes),
                'price'               => (float) $service->price,
                'shared_id'           => (string) $service->square_id,
                'allow_design_upload' => !empty($service->allow_design_upload) ? 1 : 0,
            );
        }
        foreach ((array) $rows as $option) {
            $items[] = array(
                'key'              => 'option:' . intval($option->option_id),
                'source_type'      => 'option',
                'source_id'        => intval($option->option_id),
                'service_id'       => intval($option->service_id),
                'group_label'      => $option->service_name . ' options',
                'label'            => $option->option_name,
                'name'             => $option->option_name,
                'duration_minutes' => intval($option->duration_minutes),
                'price'            => (float) $option->base_price + (float) $option->price_modifier,
                'shared_id'        => (string) $option->square_id,
            );
        }
        wp_send_json_success(array('options' => $rows, 'items' => $items));
    }

    /**
     * AJAX: add a new option row to a service (with square_id support)
     */
    public function ajax_add_option() {
        check_ajax_referer('yatoon_option', 'nonce');
        if (!current_user_can('manage_options')) { wp_send_json_error('Permission denied'); }

        $service_id       = isset($_POST['service_id'])       ? intval($_POST['service_id'])                    : 0;
        $name             = isset($_POST['name'])             ? sanitize_text_field( wp_unslash( $_POST['name'] ) )              : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $duration_minutes = isset($_POST['duration_minutes']) ? intval($_POST['duration_minutes'])               : 30;
        $price_modifier   = isset($_POST['price_modifier'])   ? floatval($_POST['price_modifier'])               : 0;
        $group_name       = isset($_POST['group_name'])       ? sanitize_text_field( wp_unslash( $_POST['group_name'] ) )        : 'Add-on'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $square_id        = isset($_POST['square_id'])        ? sanitize_text_field( wp_unslash( $_POST['square_id'] ) )         : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $square_version   = isset($_POST['square_version'])   ? sanitize_text_field( wp_unslash( $_POST['square_version'] ) )    : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $source_service_id = isset($_POST['source_service_id']) ? absint($_POST['source_service_id']) : 0;
        $source_option_id  = isset($_POST['source_option_id']) ? absint($_POST['source_option_id']) : 0;

        if (!$service_id || empty($name)) {
            wp_send_json_error('Invalid data: service_id and name are required');
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_service_options' );

        if ($source_service_id > 0) {
            $source_service = yatoon_db_get_row($wpdb->prepare(
                "SELECT square_id FROM {$wpdb->prefix}yatoon_services WHERE id = %d LIMIT 1",
                $source_service_id
            ));
            if (!$source_service) {
                wp_send_json_error('The selected source service no longer exists.');
            }
            $square_id = (string) $source_service->square_id;
        } elseif ($source_option_id > 0) {
            $source_option = yatoon_db_get_row($wpdb->prepare(
                "SELECT id, square_id FROM {$wpdb->prefix}yatoon_service_options WHERE id = %d LIMIT 1",
                $source_option_id
            ));
            if (!$source_option) {
                wp_send_json_error('The selected source option no longer exists.');
            }
            $square_id = (string) $source_option->square_id;
            if ($square_id === '') {
                $square_id = 'local_option_' . intval($source_option->id);
                yatoon_db_update(
                    $wpdb->prefix . 'yatoon_service_options',
                    array('square_id' => $square_id),
                    array('id' => intval($source_option->id)),
                    array('%s'),
                    array('%d')
                );
            }
        }

        // One-time fix: drop UNIQUE constraint on square_id so same ID can be shared across services
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $idx = yatoon_db_get_results("SHOW INDEX FROM " . $table . " WHERE Key_name = 'square_id' AND Non_unique = 0");
        if (!empty($idx)) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            yatoon_db_query("ALTER TABLE `" . $table . "` DROP INDEX square_id");
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            yatoon_db_query("ALTER TABLE `" . $table . "` ADD INDEX square_id (square_id)");
        }

        // Check for duplicate square_id within THIS service only
        if ($square_id) {
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $exists = yatoon_db_get_var($wpdb->prepare(
                "SELECT id FROM " . $table . " WHERE service_id = %d AND square_id = %s LIMIT 1",
                $service_id, $square_id
            ));
            if ($exists) {
                wp_send_json_error('This option already exists on this service.');
                return;
            }
        }

        $insert = array(
            'service_id'       => $service_id,
            'name'             => $name,
            'duration_minutes' => $duration_minutes,
            'price_modifier'   => $price_modifier,
            'group_name'       => $group_name ?: null,
            'square_id'        => $square_id,
            'sort_order'       => 99,
        );
        $format = array('%d','%s','%d','%f','%s','%s','%d');
        if ($square_version) {
            $insert['square_version'] = intval($square_version);
            $format[] = '%d';
        }

        $result = yatoon_db_insert($table, $insert, $format); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        if ($result) {
            wp_send_json_success(array(
                'message'   => 'Option added successfully',
                'option_id' => $wpdb->insert_id,
                'square_id' => $square_id,
            ));
        } else {
            wp_send_json_error('Failed to add option: ' . $wpdb->last_error);
        }
    }

        /**
     * AJAX: reorder service options by drag-drop
     */
    public function ajax_reorder_options() {
        check_ajax_referer('yatoon_option', 'nonce');
        if (!current_user_can('manage_options')) { wp_send_json_error('Permission denied'); }

        $service_id = intval($_POST['service_id'] ?? 0);
        $order_raw  = $_POST['order'] ?? '[]'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $order      = json_decode($order_raw, true);

        if (!$service_id || !is_array($order)) {
            wp_send_json_error('Invalid data');
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_service_options' );

        foreach ($order as $sort_index => $option_id) {
            yatoon_db_update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $table,
                array('sort_order' => $sort_index),
                array('id' => intval($option_id), 'service_id' => $service_id),
                array('%d'),
                array('%d', '%d')
            );
        }
        wp_send_json_success(array('message' => 'Order saved'));
    }

    public function ajax_get_staff_services() {
        check_ajax_referer('yatoon_staff_services', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }
        
        $staff_id = isset($_POST['staff_id']) ? intval($_POST['staff_id']) : 0;
        if (!$staff_id) {
            wp_send_json_error(array('message' => 'Invalid staff ID'));
        }
        
        global $wpdb;
        $services_table = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $staff_services_table = esc_sql( $wpdb->prefix . 'yatoon_staff_services' );
        $services_exists = yatoon_db_get_var($wpdb->prepare('SHOW TABLES LIKE %s', $services_table));
        if ($services_exists !== $services_table) {
            wp_send_json_error(array('message' => 'Services table is missing. Please reactivate Yatoon once to rebuild tables.'));
        }

        $service_columns = yatoon_db_get_col("SHOW COLUMNS FROM " . $services_table . "", 0); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $select = array('id', 'name');
        $select[] = in_array('price', $service_columns, true) ? 'price' : '0 AS price';
        $where = in_array('is_active', $service_columns, true) ? 'WHERE is_active = 1' : '';
        $order = array();
        if (in_array('category', $service_columns, true)) $order[] = 'category';
        if (in_array('sort_order', $service_columns, true)) $order[] = 'sort_order';
        $order[] = 'name';

        $services = yatoon_db_get_results(
            'SELECT ' . implode(', ', $select) . " FROM " . $services_table . " " . $where . " ORDER BY " . implode(', ', $order)
        ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        if ($wpdb->last_error) {
            wp_send_json_error(array('message' => 'Could not load services: ' . $wpdb->last_error));
        }

        $assigned_ids = array();
        $staff_services_exists = yatoon_db_get_var($wpdb->prepare('SHOW TABLES LIKE %s', $staff_services_table));
        if ($staff_services_exists === $staff_services_table) {
            $assigned_ids = yatoon_db_get_col($wpdb->prepare("
                SELECT service_id
                FROM " . $staff_services_table . "
                WHERE staff_id = %d
            ", $staff_id)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        }
        
        // Mark assigned services
        foreach ($services as $service) {
            $service->assigned = in_array((string) $service->id, array_map('strval', (array) $assigned_ids), true);
        }
        
        wp_send_json_success(array('services' => $services));
    }
    
    /**
     * AJAX: update staff service list
     */
    public function ajax_update_staff_services() {
        check_ajax_referer('yatoon_staff_services', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }
        
        $staff_id = isset($_POST['staff_id']) ? intval($_POST['staff_id']) : 0;
        $service_ids = isset($_POST['service_ids']) ? array_map('intval', $_POST['service_ids']) : array();
        
        if (!$staff_id) {
            wp_send_json_error(array('message' => 'Invalid staff ID'));
        }
        
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_staff_services' );
        
        // Delete all old associations
        yatoon_db_delete($table, array('staff_id' => $staff_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        
        // Insert new associations
        foreach ($service_ids as $service_id) {
            yatoon_db_insert($table, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
                'staff_id' => $staff_id,
                'service_id' => $service_id
            ));
        }
        
        wp_send_json_success(array(
            'message' => 'Services updated successfully',
            'count' => count($service_ids)
        ));
    }
    
    /**
     * AJAX: bulk delete bookings
     */
    public function ajax_delete_bookings() {
        check_ajax_referer('yatoon_delete_bookings', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $booking_ids = isset($_POST['booking_ids']) ? array_map('intval', $_POST['booking_ids']) : array();
        
        if (empty($booking_ids)) {
            wp_send_json_error('No bookings selected');
        }
        
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        $deleted = yatoon_db_query($wpdb->prepare( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "DELETE FROM `" . $table . "` WHERE id IN (" . implode(',', array_fill(0, count($booking_ids), '%d')) . ")", // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- table names use $wpdb->prefix
            $booking_ids
        ));
        
        if ($deleted !== false) {
            wp_send_json_success(array(
                'message' => sprintf('%d booking(s) deleted successfully', $deleted)
            ));
        } else {
            wp_send_json_error('Failed to delete bookings');
        }
    }

    /**
     * AJAX: delete a single booking (including multi-service group members)
     * Fixes: yatoon_delete_single_booking was called in JS but never registered.
     */
    public function ajax_delete_single_booking() {
        check_ajax_referer('yatoon_delete_bookings', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }

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

        global $wpdb;
        $table = esc_sql($wpdb->prefix . 'yatoon_bookings');

        // Find the group_id of this booking so we can delete all siblings in the same multi-service group
        $booking = yatoon_db_get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->prepare("SELECT group_id FROM `" . $table . "` WHERE id = %d", $id)
        );

        if ($booking && !empty($booking->group_id)) {
            // Delete all bookings in the same multi-service group
            $deleted = yatoon_db_query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $wpdb->prepare("DELETE FROM `" . $table . "` WHERE group_id = %s", $booking->group_id)
            );
        } else {
            // Single-service booking
            $deleted = yatoon_db_query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $wpdb->prepare("DELETE FROM `" . $table . "` WHERE id = %d", $id)
            );
        }

        if ($deleted !== false) {
            wp_send_json_success(array('message' => 'Booking deleted successfully'));
        } else {
            wp_send_json_error(array('message' => 'Failed to delete booking'));
        }
    }

    /**
     * AJAX: export bookings as CSV
     */
    public function ajax_export_bookings() {
        check_ajax_referer('yatoon_export_bookings', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_die('Permission denied');
        }
        
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $bookings = yatoon_db_get_results("SELECT * FROM " . $table . " ORDER BY created_at DESC");

        if (empty($bookings)) {
            wp_die('No bookings to export');
        }
        
        // SetCSV headers
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename=bookings-' . gmdate('Y-m-d') . '.csv');
        
        $output = fopen('php://output', 'w');
        
        // Write BOM for Excel compatibility
        fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
        
        // Write header row
        fputcsv($output, array(
            'ID', 'Customer Name', 'Email', 'Phone', 
            'Service', 'Staff', 'Date', 'Time', 
            'Duration', 'Price', 'Status', 'Created At'
        ));
        
        // Write data rows
        foreach ($bookings as $booking) {
            fputcsv($output, $this->csv_row(array(
                $booking->id,
                $booking->customer_name,
                $booking->customer_email,
                $booking->customer_phone,
                $booking->service_name,
                $booking->staff_name,
                $booking->booking_date,
                $booking->booking_time,
                $booking->duration_minutes,
                $booking->total_price,
                $booking->status,
                $booking->created_at
            )));
        }
        
        fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
        exit;
    }
    
    /**
     * AJAX: bulk delete waitlist
     */
    public function ajax_delete_waitlist() {
        check_ajax_referer('yatoon_delete_waitlist', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $ids = isset($_POST['ids']) ? array_map('intval', $_POST['ids']) : array();
        
        if (empty($ids)) {
            wp_send_json_error('No items selected');
        }
        
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_waitlist' );
        $deleted = yatoon_db_query($wpdb->prepare( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "DELETE FROM `" . $table . "` WHERE id IN (" . implode(',', array_fill(0, count($ids), '%d')) . ")", // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- table names use $wpdb->prefix
            $ids
        ));
        
        wp_send_json_success(array('message' => "$deleted item(s) deleted"));
    }
    
    /**
     * AJAX: export waitlist
     */
    public function ajax_export_waitlist() {
        check_ajax_referer('yatoon_export_waitlist', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_die('Permission denied');
        }
        
        global $wpdb;
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $items = yatoon_db_get_results("SELECT * FROM {$wpdb->prefix}yatoon_waitlist ORDER BY created_at DESC");

        if (empty($items)) wp_die('No data');
        
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename=waitlist-' . gmdate('Y-m-d') . '.csv');
        
        $output = fopen('php://output', 'w');
        fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
        
        fputcsv($output, array('ID', 'Name', 'Email', 'Phone', 'Service', 'Staff', 'Date', 'Flexible Dates', 'Flexible Times', 'Notes', 'Created'));
        
        foreach ($items as $item) {
            fputcsv($output, $this->csv_row(array(
                $item->id,
                $item->customer_name,
                $item->customer_email,
                $item->customer_phone,
                $item->service_id,
                $item->staff_id,
                $item->preferred_date,
                $item->flexible_dates ? 'Yes' : 'No',
                $item->flexible_times ? 'Yes' : 'No',
                $item->notes,
                $item->created_at
            )));
        }
        
        fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
        exit;
    }

    /**
     * AJAX: export reports dashboard as CSV
     */
    public function ajax_export_reports() {
        check_ajax_referer( 'yatoon_export_reports', 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Permission denied' );
        }

        global $wpdb;
        $date_from = isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : gmdate( 'Y-m-d', strtotime( '-29 days', current_time( 'timestamp' ) ) );
        $date_to   = isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : current_time( 'Y-m-d' );

        if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_from ) ) {
            $date_from = gmdate( 'Y-m-d', strtotime( '-29 days', current_time( 'timestamp' ) ) );
        }
        if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_to ) ) {
            $date_to = current_time( 'Y-m-d' );
        }
        if ( $date_from > $date_to ) {
            $tmp       = $date_from;
            $date_from = $date_to;
            $date_to   = $tmp;
        }

        $bookings_table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        $services_table = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $staff_table    = esc_sql( $wpdb->prefix . 'yatoon_staff' );

        $summary = yatoon_db_get_row( $wpdb->prepare(
            "SELECT COUNT(id) AS total_bookings,
                    SUM(CASE WHEN status IN ('completed','COMPLETED') THEN 1 ELSE 0 END) AS completed_count,
                    SUM(CASE WHEN status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                    SUM(CASE WHEN status LIKE 'cancelled%%' OR status LIKE 'CANCELLED%%' OR status = 'DECLINED' THEN 1 ELSE 0 END) AS cancelled_count,
                    SUM(CASE WHEN payment_status IS NULL OR payment_status = 'unpaid' THEN 1 ELSE 0 END) AS unpaid_count,
                    SUM(CASE WHEN status NOT LIKE 'cancelled%%' AND status NOT LIKE 'CANCELLED%%' AND status != 'DECLINED' THEN total_price ELSE 0 END) AS scheduled_revenue
             FROM " . $bookings_table . "
             WHERE DATE(start_time) BETWEEN %s AND %s",
            $date_from,
            $date_to
        ) );

        $top_services = yatoon_db_get_results( $wpdb->prepare(
            "SELECT s.name, COUNT(b.id) AS booking_count, SUM(b.total_price) AS total_revenue
             FROM " . $bookings_table . " b
             LEFT JOIN " . $services_table . " s ON b.service_id = s.id
             WHERE DATE(b.start_time) BETWEEN %s AND %s
             GROUP BY b.service_id, s.name
             ORDER BY booking_count DESC, total_revenue DESC
             LIMIT 50",
            $date_from,
            $date_to
        ) );

        $staff_performance = yatoon_db_get_results( $wpdb->prepare(
            "SELECT st.name, COUNT(b.id) AS booking_count,
                    SUM(CASE WHEN b.status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                    SUM(b.total_price) AS total_revenue
             FROM " . $bookings_table . " b
             LEFT JOIN " . $staff_table . " st ON b.staff_id = st.id
             WHERE DATE(b.start_time) BETWEEN %s AND %s
             GROUP BY b.staff_id, st.name
             ORDER BY booking_count DESC, total_revenue DESC
             LIMIT 50",
            $date_from,
            $date_to
        ) );

        header( 'Content-Type: text/csv; charset=utf-8' );
        header( 'Content-Disposition: attachment; filename=reports-' . $date_from . '-to-' . $date_to . '.csv' );

        $output = fopen( 'php://output', 'w' );
        fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) );

        fputcsv( $output, array( 'Yatoon Reports', $date_from . ' to ' . $date_to ) );
        fputcsv( $output, array() );
        fputcsv( $output, array( 'Summary' ) );
        fputcsv( $output, array( 'Bookings', 'Completed', 'No Shows', 'Cancelled', 'Unpaid', 'Scheduled Revenue' ) );
        fputcsv( $output, array(
            intval( $summary->total_bookings ?? 0 ),
            intval( $summary->completed_count ?? 0 ),
            intval( $summary->no_show_count ?? 0 ),
            intval( $summary->cancelled_count ?? 0 ),
            intval( $summary->unpaid_count ?? 0 ),
            number_format( (float) ( $summary->scheduled_revenue ?? 0 ), 2, '.', '' ),
        ) );

        fputcsv( $output, array() );
        fputcsv( $output, array( 'Top Services' ) );
        fputcsv( $output, array( 'Service', 'Bookings', 'Revenue' ) );
        foreach ( $top_services as $row ) {
            fputcsv( $output, $this->csv_row( array( $row->name ?: 'Unknown Service', intval( $row->booking_count ), number_format( (float) $row->total_revenue, 2, '.', '' ) ) ) );
        }

        fputcsv( $output, array() );
        fputcsv( $output, array( 'Staff Performance' ) );
        fputcsv( $output, array( 'Staff', 'Bookings', 'No Shows', 'Revenue' ) );
        foreach ( $staff_performance as $row ) {
            fputcsv( $output, $this->csv_row( array( $row->name ?: 'Any / Unassigned', intval( $row->booking_count ), intval( $row->no_show_count ), number_format( (float) $row->total_revenue, 2, '.', '' ) ) ) );
        }

        fclose( $output ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
        exit;
    }

    public function ajax_check_waitlist_now() {
        check_ajax_referer('yatoon_waitlist_check', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $cron = new YATOON_Cron();
        $result = $cron->check_waitlist_availability(50);
        wp_send_json_success($result);
    }

    public function ajax_notify_waitlist_customer() {
        check_ajax_referer('yatoon_waitlist_notify', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        global $wpdb;
        $id = intval($_POST['id'] ?? 0);
        $table = esc_sql( $wpdb->prefix . 'yatoon_waitlist' );
        $item = yatoon_db_get_row($wpdb->prepare("SELECT * FROM `" . $table . "` WHERE id = %d", $id));
        if (!$item || empty($item->customer_email)) {
            wp_send_json_error(array('message' => 'Waitlist request or email not found.'));
        }

        $business_name = get_option('yatoon_business_name', get_bloginfo('name'));
        $booking_url = get_option('yatoon_book_new_url', '');
        if (!$booking_url) {
            $booking_page = get_page_by_path('booking');
            $booking_url = $booking_page ? get_permalink($booking_page->ID) : home_url('/');
        }
        $subject = '[' . $business_name . '] An appointment opening may be available';
        $body = "Hi {$item->customer_name},\n\nAn opening may be available for your waitlist request.";
        if (!empty($item->preferred_date)) {
            $body .= "\nPreferred date: " . date_i18n('F j, Y', strtotime($item->preferred_date));
        }
        if (!empty($item->preferred_time)) {
            $body .= "\nPreferred time: {$item->preferred_time}";
        }
        $body .= "\n\nBook here: {$booking_url}\n\n- {$business_name}";

        $sent = wp_mail($item->customer_email, $subject, nl2br(esc_html($body)), array('Content-Type: text/html; charset=UTF-8'));
        if (!$sent) {
            wp_send_json_error(array('message' => 'Email could not be sent.'));
        }

        yatoon_db_update($table, array(
            'notified' => 1,
            'notified_at' => current_time('mysql'),
        ), array('id' => $id));
        wp_send_json_success(array('message' => 'Customer notified.'));
    }

    /**
     * AJAX: Save client notes/profile
     */
    public function ajax_save_client_notes() {
        check_ajax_referer( 'yatoon_client_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error();

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_clients' );

        $id              = isset( $_POST['id'] )             ? intval( $_POST['id'] )                                            : 0;
        $notes           = isset( $_POST['notes'] )          ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) )          : '';
        $allergies       = isset( $_POST['allergies'] )      ? sanitize_textarea_field( wp_unslash( $_POST['allergies'] ) )      : '';
        $preferences     = isset( $_POST['preferences'] )    ? sanitize_textarea_field( wp_unslash( $_POST['preferences'] ) )    : '';
        $preferred_staff = isset( $_POST['preferred_staff'] )? sanitize_text_field( wp_unslash( $_POST['preferred_staff'] ) )    : '';
        $tags            = isset( $_POST['tags'] )           ? sanitize_text_field( wp_unslash( $_POST['tags'] ) )              : '';
        $customer_name   = isset( $_POST['customer_name'] )  ? sanitize_text_field( wp_unslash( $_POST['customer_name'] ) )     : '';

        if ( ! $id ) { wp_send_json_error( array( 'message' => 'Invalid client ID' ) ); }

        yatoon_db_update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $table,
            array(
                'notes'           => $notes,
                'allergies'       => $allergies,
                'preferences'     => $preferences,
                'preferred_staff' => $preferred_staff,
                'tags'            => $tags,
                'customer_name'   => $customer_name,
            ),
            array( 'id' => $id )
        );
        wp_send_json_success( array( 'message' => 'Client profile saved.' ) );
    }

    /**
     * AJAX: Get single client data (for modal)
     */
    public function ajax_get_client() {
        check_ajax_referer( 'yatoon_client_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error();

        global $wpdb;
        $id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
        if ( ! $id ) wp_send_json_error();

        $client = yatoon_db_get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT * FROM {$wpdb->prefix}yatoon_clients WHERE id = %d",
            $id
        ) );
        if ( ! $client ) { wp_send_json_error( array( 'message' => 'Client not found' ) ); }

        $bookings = yatoon_db_get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT b.start_time, b.end_time, b.status, b.payment_status, b.total_price, b.notes,
                    b.consent_accepted, b.policy_acknowledged,
                    s.name AS service_name, st.name AS staff_name
             FROM {$wpdb->prefix}yatoon_bookings b
             LEFT JOIN {$wpdb->prefix}yatoon_services s  ON b.service_id = s.id
             LEFT JOIN {$wpdb->prefix}yatoon_staff   st ON b.staff_id   = st.id
             WHERE b.customer_phone = %s OR b.customer_email = %s
             ORDER BY b.start_time DESC LIMIT 50",
            $client->customer_phone, $client->customer_email
        ) );

        $staff_list = yatoon_db_get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT id, name FROM {$wpdb->prefix}yatoon_staff WHERE is_active = 1 ORDER BY name ASC"
        );

        $stats = yatoon_db_get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT
                COUNT(id) AS total_bookings,
                SUM(CASE WHEN status IN ('completed','COMPLETED') THEN 1 ELSE 0 END) AS completed_count,
                SUM(CASE WHEN status IN ('no_show','NO_SHOW') THEN 1 ELSE 0 END) AS no_show_count,
                SUM(CASE WHEN status LIKE 'cancelled%%' OR status LIKE 'CANCELLED%%' THEN 1 ELSE 0 END) AS cancelled_count,
                SUM(CASE WHEN payment_status IS NULL OR payment_status = 'unpaid' THEN 1 ELSE 0 END) AS unpaid_count,
                SUM(total_price) AS lifetime_value,
                MAX(start_time) AS last_visit
             FROM {$wpdb->prefix}yatoon_bookings
             WHERE customer_phone = %s OR customer_email = %s",
            $client->customer_phone, $client->customer_email
        ) );

        wp_send_json_success( array(
            'client'     => $client,
            'bookings'   => $bookings,
            'staff_list' => $staff_list,
            'stats'      => $stats,
        ) );
    }

    /**
     * AJAX: bulk delete messages
     */
    public function ajax_delete_messages() {
        check_ajax_referer('yatoon_delete_messages', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $ids = isset($_POST['ids']) ? array_map('intval', $_POST['ids']) : array();
        
        if (empty($ids)) {
            wp_send_json_error('No messages selected');
        }
        
        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_contact_messages' );
        $deleted = yatoon_db_query($wpdb->prepare( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "DELETE FROM `" . $table . "` WHERE id IN (" . implode(',', array_fill(0, count($ids), '%d')) . ")", // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- table names use $wpdb->prefix
            $ids
        ));
        
        wp_send_json_success(array('message' => "$deleted message(s) deleted"));
    }

    /**
     * AJAX: update booking operational status
     */
    public function ajax_update_booking_status() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $id = isset($_POST['booking_id']) ? intval($_POST['booking_id']) : 0;
        $status = isset($_POST['status']) ? sanitize_key(wp_unslash($_POST['status'])) : '';
        $allowed = array('pending', 'confirmed', 'completed', 'no_show', 'cancelled');
        if (!$id || !in_array($status, $allowed, true)) {
            wp_send_json_error('Invalid booking status');
        }

        global $wpdb;
        $result = yatoon_db_update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prefix . 'yatoon_bookings',
            array('status' => $status, 'updated_at' => current_time('mysql')),
            array('id' => $id),
            array('%s', '%s'),
            array('%d')
        );

        if ($result === false) {
            wp_send_json_error('Failed to update booking status');
        }
        if ($status === 'no_show') {
            do_action('yatoon_booking_marked_no_show', $id);
        }
        do_action( 'yatoon_booking_status_changed', $id, $status );
        wp_send_json_success(array('message' => 'Booking status updated.'));
    }

    /**
     * AJAX: update booking payment status
     */
    public function ajax_update_payment_status() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $id = isset($_POST['booking_id']) ? intval($_POST['booking_id']) : 0;
        $payment_status = isset($_POST['payment_status']) ? sanitize_key(wp_unslash($_POST['payment_status'])) : '';
        $note = isset($_POST['payment_note']) ? sanitize_textarea_field(wp_unslash($_POST['payment_note'])) : '';
        $allowed = array('unpaid', 'paid_square_pos', 'deposit_paid', 'refunded', 'waived', 'no_show_fee_charged', 'no_show_fee_waived');
        if (!$id || !in_array($payment_status, $allowed, true)) {
            wp_send_json_error('Invalid payment status');
        }

        global $wpdb;
        $result = yatoon_db_update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prefix . 'yatoon_bookings',
            array(
                'payment_status'     => $payment_status,
                'payment_note'       => $note,
                'payment_updated_at' => current_time('mysql'),
                'updated_at'         => current_time('mysql'),
            ),
            array('id' => $id),
            array('%s', '%s', '%s', '%s'),
            array('%d')
        );

        if ($result === false) {
            wp_send_json_error('Failed to update payment status');
        }
        $booking_row = yatoon_db_get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_bookings WHERE id = %d LIMIT 1", $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $booking_row ) {
            do_action( 'yatoon_payment_status_changed', $id, $payment_status, $booking_row );
        }
        wp_send_json_success(array('message' => 'Payment status updated.'));
    }

    public function ajax_get_admin_reschedule_slots() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error(array('message' => 'Permission denied'));

        $booking_id = isset($_POST['booking_id']) ? absint($_POST['booking_id']) : 0;
        $staff_id   = isset($_POST['staff_id']) ? absint($_POST['staff_id']) : 0;
        $date       = isset($_POST['date']) ? sanitize_text_field(wp_unslash($_POST['date'])) : '';

        if (!$booking_id || !$staff_id || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            wp_send_json_error(array('message' => 'Invalid reschedule request.'));
        }

        global $wpdb;
        $booking = yatoon_db_get_row($wpdb->prepare(
            "SELECT id, service_id FROM {$wpdb->prefix}yatoon_bookings WHERE id = %d LIMIT 1",
            $booking_id
        ));
        if (!$booking) {
            wp_send_json_error(array('message' => 'Booking not found.'));
        }

        $slots = class_exists('YATOON_Business_Hours')
            ? YATOON_Business_Hours::get_available_slots_for_date($date, intval($booking->service_id), $staff_id, $booking_id)
            : array();

        wp_send_json_success(array(
            'slots' => array_map(function($slot) {
                return array(
                    'display'  => $slot['display'] ?? '',
                    'datetime' => $slot['datetime'] ?? '',
                );
            }, (array) $slots),
        ));
    }

    public function ajax_admin_reschedule_booking() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error(array('message' => 'Permission denied'));

        if (get_option('yatoon_active_platform', 'local') !== 'local') {
            wp_send_json_error(array('message' => 'Drag reschedule is available for Local booking mode. Update Square/Vagaro appointments in the source calendar, then sync.'));
        }

        $booking_id = isset($_POST['booking_id']) ? absint($_POST['booking_id']) : 0;
        $staff_id   = isset($_POST['staff_id']) ? absint($_POST['staff_id']) : 0;
        $start_raw  = isset($_POST['start_at']) ? sanitize_text_field(wp_unslash($_POST['start_at'])) : '';

        // notify_method: 'none' | 'email' | 'sms' | 'both'. Falls back to the legacy
        // boolean notify_customer (checkbox) so any cached/old JS keeps working.
        $notify_method = isset($_POST['notify_method']) ? sanitize_key(wp_unslash($_POST['notify_method'])) : '';
        if (!in_array($notify_method, array('none', 'email', 'sms', 'both'), true)) {
            $notify_method = !empty($_POST['notify_customer']) ? 'email' : 'none';
        }

        if (!$booking_id || !$staff_id || !$start_raw) {
            wp_send_json_error(array('message' => 'Missing reschedule details.'));
        }

        global $wpdb;
        $booking = yatoon_db_get_row($wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}yatoon_bookings WHERE id = %d LIMIT 1",
            $booking_id
        ));
        if (!$booking) {
            wp_send_json_error(array('message' => 'Booking not found.'));
        }

        try {
            $tz       = wp_timezone();
            $start_dt = new DateTime($start_raw, $tz);
        } catch (Exception $e) {
            wp_send_json_error(array('message' => 'Invalid date or time.'));
        }

        $duration_min = max(1, intval((strtotime($booking->end_time) - strtotime($booking->start_time)) / 60));
        $end_dt       = clone $start_dt;
        $end_dt->modify("+{$duration_min} minutes");

        if (!class_exists('YATOON_Business_Hours') || !YATOON_Business_Hours::is_valid_slot(
            $start_dt->format('Y-m-d H:i:s'),
            intval($booking->service_id),
            $staff_id,
            $booking_id
        )) {
            wp_send_json_error(array('message' => 'That time is no longer available for this staff member.'));
        }

        $updated = yatoon_db_update(
            $wpdb->prefix . 'yatoon_bookings',
            array(
                'staff_id'    => $staff_id,
                'start_time'  => $start_dt->format('Y-m-d H:i:s'),
                'end_time'    => $end_dt->format('Y-m-d H:i:s'),
                'status'      => 'pending',
                'updated_at'  => current_time('mysql'),
            ),
            array('id' => $booking_id),
            array('%d', '%s', '%s', '%s', '%s'),
            array('%d')
        );

        if ($updated === false) {
            wp_send_json_error(array('message' => 'Could not reschedule booking.'));
        }

        if ($notify_method !== 'none') {
            // Join service/staff names so the email + SMS templates render properly
            // (the bare bookings row only has service_id/staff_id).
            $fresh = yatoon_db_get_row($wpdb->prepare(
                "SELECT b.*, s.name AS service_name, st.name AS staff_name
                 FROM {$wpdb->prefix}yatoon_bookings b
                 LEFT JOIN {$wpdb->prefix}yatoon_services s ON b.service_id = s.id
                 LEFT JOIN {$wpdb->prefix}yatoon_staff st ON b.staff_id = st.id
                 WHERE b.id = %d LIMIT 1",
                $booking_id
            ));
            if ($fresh) {
                $fresh_arr = (array) $fresh;
                if (in_array($notify_method, array('email', 'both'), true) && class_exists('YATOON_Notifications')) {
                    $notif = new YATOON_Notifications();
                    $notif->send_reschedule_email($fresh_arr);
                }
                if (in_array($notify_method, array('sms', 'both'), true) && class_exists('YATOON_Twilio')) {
                    YATOON_Twilio::send_reschedule_sms($fresh_arr);
                }
            }
        }

        wp_send_json_success(array(
            'message'    => 'Booking rescheduled.',
            'start_time' => $start_dt->format('g:i A'),
            'end_time'   => $end_dt->format('g:i A'),
            'date'       => $start_dt->format('Y-m-d'),
            'staff_id'   => $staff_id,
        ));
    }

    public function ajax_save_calendar_staff_filter() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');
        $raw = isset($_POST['hidden_staff']) ? sanitize_text_field(wp_unslash($_POST['hidden_staff'])) : '';
        // Validate: only digits and commas
        if ($raw !== '' && !preg_match('/^[\d,]+$/', $raw)) wp_send_json_error('Invalid input');
        update_option('yatoon_calendar_hidden_staff', $raw);
        wp_send_json_success();
    }

    public function ajax_save_calendar_timezone() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');
        $tz = isset($_POST['timezone']) ? sanitize_text_field(wp_unslash($_POST['timezone'])) : '';
        // Validate against known PHP timezone identifiers
        if (!in_array($tz, timezone_identifiers_list(), true)) wp_send_json_error('Invalid timezone');
        update_option('yatoon_calendar_timezone', $tz);
        wp_send_json_success();
    }

    public function ajax_send_payment_request() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }

        $booking_id = isset($_POST['booking_id']) ? absint($_POST['booking_id']) : 0;
        if (!$booking_id) {
            wp_send_json_error(array('message' => 'Invalid booking.'));
        }

        global $wpdb;
        $booking = yatoon_db_get_row($wpdb->prepare(
            "SELECT b.*, s.name AS service_name, st.name AS staff_name
             FROM {$wpdb->prefix}yatoon_bookings b
             LEFT JOIN {$wpdb->prefix}yatoon_services s ON b.service_id = s.id
             LEFT JOIN {$wpdb->prefix}yatoon_staff st ON b.staff_id = st.id
             WHERE b.id = %d LIMIT 1",
            $booking_id
        ));
        if (!$booking || empty($booking->customer_email)) {
            wp_send_json_error(array('message' => 'Booking email not found.'));
        }

        $business = get_option('yatoon_business_name', get_bloginfo('name'));
        $pay_url = get_option('yatoon_payment_request_url', '');
        if (!$pay_url && !empty($booking->manage_token) && class_exists('Yatoon_Manage_Booking')) {
            $pay_url = Yatoon_Manage_Booking::get_manage_url($booking->manage_token);
        }

        /* translators: %s: business name. */
        $subject = sprintf(__('Payment reminder for your appointment at %s', 'yatoon-booking-system'), $business);
        $body = '<div style="font-family:Arial,sans-serif;max-width:560px;margin:0 auto;padding:20px;color:#111827;">'
              . '<h2 style="margin-top:0;">' . esc_html($business) . '</h2>'
              . '<p>Hi ' . esc_html($booking->customer_name) . ',</p>'
              . '<p>This is a friendly payment reminder for your appointment.</p>'
              . '<p><strong>Service:</strong> ' . esc_html($booking->service_name ?: 'Appointment') . '<br>'
              . '<strong>Date:</strong> ' . esc_html(gmdate('F j, Y g:i A', strtotime($booking->start_time))) . '<br>'
              . '<strong>Total:</strong> $' . esc_html(number_format((float) $booking->total_price, 2)) . '</p>';
        if ($pay_url) {
            $body .= '<p><a href="' . esc_url($pay_url) . '" style="display:inline-block;background:#111827;color:#fff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:700;">Review payment details</a></p>';
        }
        $body .= '<p>If you already paid, please ignore this message. Thank you!</p></div>';

        $sent = wp_mail($booking->customer_email, $subject, $body, array('Content-Type: text/html; charset=UTF-8'));
        if (!$sent) {
            wp_send_json_error(array('message' => 'Email failed.'));
        }
        yatoon_db_update(
            $wpdb->prefix . 'yatoon_bookings',
            array(
                'payment_note' => trim(($booking->payment_note ? $booking->payment_note . "\n" : '') . 'Payment request sent ' . current_time('mysql')),
                'payment_updated_at' => current_time('mysql'),
            ),
            array('id' => $booking_id),
            array('%s', '%s'),
            array('%d')
        );
        wp_send_json_success(array('message' => 'Payment request sent.'));
    }

    public function ajax_send_recall_email() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }
        $name  = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : 'there';
        $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
        if (!$email || !is_email($email)) {
            wp_send_json_error(array('message' => 'Customer email missing.'));
        }
        $business = get_option('yatoon_business_name', get_bloginfo('name'));
        $book_url = get_option('yatoon_book_new_url', '');
        /* translators: %s: business name. */
        $subject = sprintf(__('We would love to see you again at %s', 'yatoon-booking-system'), $business);
        $body = '<div style="font-family:Arial,sans-serif;max-width:560px;margin:0 auto;padding:20px;color:#111827;">'
              . '<h2 style="margin-top:0;">' . esc_html($business) . '</h2>'
              . '<p>Hi ' . esc_html($name ?: 'there') . ',</p>'
              . '<p>It has been a little while since your last visit. We would love to welcome you back.</p>';
        if ($book_url) {
            $body .= '<p><a href="' . esc_url($book_url) . '" style="display:inline-block;background:#111827;color:#fff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:700;">Book again</a></p>';
        }
        $body .= '<p>Thank you for choosing us.</p></div>';
        $sent = wp_mail($email, $subject, $body, array('Content-Type: text/html; charset=UTF-8'));
        if (!$sent) {
            wp_send_json_error(array('message' => 'Email failed.'));
        }
        wp_send_json_success(array('message' => 'Recall email sent.'));
    }

    public function ajax_twilio_test() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            wp_send_json_error(array('message' => 'SMS testing requires an active Pro license.'));
        }
        if (!class_exists('YATOON_Twilio')) {
            wp_send_json_error(array('message' => 'Twilio class is not available.'));
        }
        $phone = isset($_POST['test_phone']) ? sanitize_text_field(wp_unslash($_POST['test_phone'])) : '';
        $result = YATOON_Twilio::test_send($phone);
        if (!empty($result['success'])) {
            wp_send_json_success(array('message' => $result['message']));
        }
        wp_send_json_error(array('message' => $result['message'] ?? 'Test failed.'));
    }
    
    /**
     * AJAX: export messages
     */
    public function ajax_export_messages() {
        check_ajax_referer('yatoon_export_messages', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_die('Permission denied');
        }
        
        global $wpdb;
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $messages = yatoon_db_get_results("SELECT * FROM {$wpdb->prefix}yatoon_contact_messages ORDER BY created_at DESC");

        if (empty($messages)) wp_die('No messages');
        
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename=messages-' . gmdate('Y-m-d') . '.csv');
        
        $output = fopen('php://output', 'w');
        fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
        
        fputcsv($output, array('ID', 'Name', 'Phone', 'Message', 'IP', 'Created'));
        
        foreach ($messages as $msg) {
            fputcsv($output, $this->csv_row(array(
                $msg->id,
                $msg->name,
                $msg->phone,
                $msg->message,
                $msg->ip_address,
                $msg->created_at
            )));
        }
        
        fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
        exit;
    }
    
    /**
     * AJAX: Get Service
     */
    public function ajax_get_service() {
        check_ajax_referer('yatoon_service', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $service_id = intval($_POST['service_id']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        
        global $wpdb;
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $service = yatoon_db_get_row($wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}yatoon_services WHERE id = %d",
            $service_id
        ));
        
        if ($service) {
            wp_send_json_success($service);
        } else {
            wp_send_json_error('Service not found');
        }
    }
    
    /**
     * AJAX: Save Service (Add or Update)
     */
    public function ajax_save_service() {
        check_ajax_referer('yatoon_service', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
        
        global $wpdb;

        $name = sanitize_text_field( wp_unslash( $_POST['name'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $description = sanitize_textarea_field( wp_unslash( $_POST['description'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $duration = intval($_POST['duration']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        $price = floatval($_POST['price']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        $category = sanitize_text_field( wp_unslash( $_POST['category'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        $image_url = isset($_POST['image_url']) ? esc_url_raw( wp_unslash( $_POST['image_url'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        $is_active = intval($_POST['is_active']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        $require_policy = isset($_POST['require_policy']) ? intval($_POST['require_policy']) : 0;
        $require_consent = isset($_POST['require_consent']) ? intval($_POST['require_consent']) : 0;
        $allow_staff_select = isset($_POST['allow_staff_select']) ? intval($_POST['allow_staff_select']) : 1;
        $allow_design_upload = isset($_POST['allow_design_upload']) ? intval($_POST['allow_design_upload']) : 0;
        $is_featured = isset($_POST['is_featured']) ? intval($_POST['is_featured']) : 0;
        $buffer_before_min = isset($_POST['buffer_before_min']) ? max(0, intval($_POST['buffer_before_min'])) : 0;
        $buffer_after_min = isset($_POST['buffer_after_min']) ? max(0, intval($_POST['buffer_after_min'])) : 0;
        $min_lead_time_min = isset($_POST['min_lead_time_min']) ? max(0, intval($_POST['min_lead_time_min'])) : 0;
        
        if (empty($name) || $duration <= 0 || $price < 0) {
            wp_send_json_error('Invalid data: name, duration, and price are required');
        }
        
        $table = esc_sql( $wpdb->prefix . 'yatoon_services' );

        $design_column_exists = (bool) yatoon_db_get_var(
            $wpdb->prepare( "SHOW COLUMNS FROM `{$table}` LIKE %s", 'allow_design_upload' )
        );
        if (!$design_column_exists) {
            yatoon_db_query(
                "ALTER TABLE `{$table}`
                 ADD COLUMN allow_design_upload tinyint(1) DEFAULT 0 AFTER allow_staff_select"
            );
            $design_column_exists = (bool) yatoon_db_get_var(
                $wpdb->prepare( "SHOW COLUMNS FROM `{$table}` LIKE %s", 'allow_design_upload' )
            );
        }
        if (!$design_column_exists) {
            wp_send_json_error('Could not add the design upload setting to the services table. Please check the database user ALTER permission.');
        }
        
        $data = array(
            'name' => $name,
            'description' => $description,
            'duration_minutes' => $duration,
            'price' => $price,
            'category' => $category,
            'image_url' => $image_url,
            'is_active' => $is_active,
            'require_policy' => $require_policy ? 1 : 0,
            'require_consent' => $require_consent ? 1 : 0,
            'allow_staff_select' => $allow_staff_select ? 1 : 0,
            'allow_design_upload' => $allow_design_upload ? 1 : 0,
            'is_featured' => $is_featured ? 1 : 0,
            'buffer_before_min' => $buffer_before_min,
            'buffer_after_min' => $buffer_after_min,
            'min_lead_time_min' => $min_lead_time_min,
            'updated_at' => current_time('mysql')
        );
        
        if ($id > 0) {
            // Update existing service
            $result = yatoon_db_update(
                $table,
                $data,
                array('id' => $id),
                array('%s', '%s', '%d', '%f', '%s', '%s', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s'),
                array('%d')
            ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $message = 'Service updated successfully';
        } else {
            // Insert new service
            $data['square_id'] = 'custom_' . time();
            $data['created_at'] = current_time('mysql');
            
            // Get max sort_order
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $max_order = yatoon_db_get_var("SELECT MAX(sort_order) FROM " . $table . "");
            $data['sort_order'] = ($max_order ? $max_order : 0) + 1;

            $result = yatoon_db_insert($table, $data);
            $message = 'Service created successfully';
        }

        if ($result !== false) {
            $service_id = $id > 0 ? $id : $wpdb->insert_id;
            wp_send_json_success(array('message' => $message, 'service_id' => $service_id));
        } else {
            $db_error = sanitize_text_field((string) $wpdb->last_error);
            wp_send_json_error($db_error ? 'Failed to save service: ' . $db_error : 'Failed to save service');
        }
    }

    /**
     * AJAX: Rename all services in a category.
     */
    public function ajax_rename_service_category() {
        check_ajax_referer('yatoon_service_category', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $old_category = isset($_POST['old_category']) ? sanitize_text_field(wp_unslash($_POST['old_category'])) : '';
        $new_category = isset($_POST['new_category']) ? sanitize_text_field(wp_unslash($_POST['new_category'])) : '';

        if ($old_category === '' || $new_category === '') {
            wp_send_json_error('Category names are required');
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $result = yatoon_db_update(
            $table,
            array(
                'category' => $new_category,
                'updated_at' => current_time('mysql'),
            ),
            array('category' => $old_category),
            array('%s', '%s'),
            array('%s')
        );

        if ($result !== false) {
            wp_send_json_success(array('updated' => (int) $result));
        }

        wp_send_json_error('Failed to rename category');
    }

    /**
     * AJAX: Delete a category by clearing it from services.
     */
    public function ajax_delete_service_category() {
        check_ajax_referer('yatoon_service_category', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }

        $category = isset($_POST['category']) ? sanitize_text_field(wp_unslash($_POST['category'])) : '';

        if ($category === '') {
            wp_send_json_error('Category is required');
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_services' );
        $result = yatoon_db_update(
            $table,
            array(
                'category' => '',
                'updated_at' => current_time('mysql'),
            ),
            array('category' => $category),
            array('%s', '%s'),
            array('%s')
        );

        if ($result !== false) {
            wp_send_json_success(array('updated' => (int) $result));
        }

        wp_send_json_error('Failed to delete category');
    }

    public function ajax_save_service_images() {
        check_ajax_referer('yatoon_service', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');

        $service_id = absint($_POST['service_id'] ?? 0);
        if (!$service_id) wp_send_json_error('Missing service ID');

        $images = isset($_POST['images']) ? (array)$_POST['images'] : array();

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_service_images' );

        // Replace all images for this service
        yatoon_db_delete($table, array('service_id' => $service_id));

        foreach ($images as $i => $img) {
            $url     = esc_url_raw(wp_unslash($img['url'] ?? ''));
            $caption = sanitize_text_field(wp_unslash($img['caption'] ?? ''));
            if (!$url) continue;
            yatoon_db_insert($table, array(
                'service_id' => $service_id,
                'image_url'  => $url,
                'caption'    => $caption,
                'sort_order' => intval($i),
            ));
        }

        wp_send_json_success(array('saved' => count($images)));
    }

    public function ajax_get_service_images() {
        check_ajax_referer('yatoon_service', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');

        $service_id = absint($_GET['service_id'] ?? $_POST['service_id'] ?? 0);
        if (!$service_id) wp_send_json_success(array('images' => array()));

        global $wpdb;
        $table  = esc_sql( $wpdb->prefix . 'yatoon_service_images' );
        $images = yatoon_db_get_results($wpdb->prepare(
            "SELECT id, image_url, caption, sort_order FROM " . $table . " WHERE service_id = %d ORDER BY sort_order ASC",
            $service_id
        ));

        wp_send_json_success(array('images' => $images));
    }
    
    /**
     * AJAX: Toggle Service Active Status
     */
    public function ajax_toggle_service() {
        check_ajax_referer('yatoon_service', 'nonce');
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        $service_id = intval($_POST['service_id']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        $is_active = intval($_POST['is_active']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
        
        global $wpdb;
        $result = yatoon_db_update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prefix . 'yatoon_services',
            array('is_active' => $is_active),
            array('id' => $service_id)
        );
        
        if ($result !== false) {
            wp_send_json_success();
        } else {
            wp_send_json_error('Failed to update status');
        }
    }

    /**
     * Display service menu admin page
     */
    public function display_service_menu_page() {
        if ( ! current_user_can( 'manage_options' ) ) return;
        include YATOON_PLUGIN_DIR . 'admin/views/service-menu.php';
    }

}
