<?php
// phpcs:disable 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_export_reports_pdf', array($this, 'ajax_export_reports_pdf'));
        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_whatsapp_test', array($this, 'ajax_whatsapp_test'));
        add_action('wp_ajax_yatoon_save_client_notes', array($this, 'ajax_save_client_notes'));
        add_action('wp_ajax_yatoon_delete_clients', array($this, 'ajax_delete_clients'));
        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_integrations_settings', array( $this, 'handle_save_integrations' ) );
        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>' . esc_html__( 'Yatoon Booking is almost ready.', 'yatoon-booking-system' ) . '</strong> ' . esc_html__( 'Add at least one service and one staff member to start accepting bookings.', 'yatoon-booking-system' ) . '</p><p><a class="button button-primary" href="' . esc_url(admin_url('admin.php?page=sbs-services')) . '">' . esc_html__( 'Set up services', 'yatoon-booking-system' ) . '</a> <a class="button" href="' . esc_url(admin_url('admin.php?page=sbs-staff')) . '">' . esc_html__( 'Set up staff', 'yatoon-booking-system' ) . '</a></p></div>';
            return;
        }

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

        // -- WordPress silently re-parents this whole menu (breaking direct-URL
        // access to EVERY other submenu below with a "Sorry, you are not allowed
        // to access this page" 403) unless the very first add_submenu_page() call
        // reuses the top-level menu's own slug. Keep this registration first --
        // it is otherwise invisible to merchants since the App Shell (see
        // class-yatoon-app-shell.php) hides the native WP submenu entirely and
        // renders its own nav from a fixed order, independent of this list.
        add_submenu_page( 'yatoon-booking-system', 'Settings', 'Settings', 'manage_options', 'yatoon-booking-system', array($this, 'display_settings_page') );

        // -- 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') );
        if ( ! class_exists( 'YATOON_Feature_Modules' ) || YATOON_Feature_Modules::enabled( 'insights' ) ) { add_submenu_page( 'yatoon-booking-system', 'Reports', 'Reports', 'manage_options', 'yatoon-reports', array($this, 'display_reports_page') ); }
        if ( ! class_exists( 'YATOON_Feature_Modules' ) || YATOON_Feature_Modules::enabled( 'ai' ) ) { 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') );

        // -- 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', 'yatoon-booking-system' ) );
        }

        $protected_empty_keys = array(
            'yatoon_square_access_token',
            'yatoon_vagaro_api_key',
            'yatoon_vagaro_client_secret',
            'yatoon_deposit_stripe_secret',
            'yatoon_stripe_webhook_secret',
            'yatoon_square_webhook_signature_key',
            'yatoon_twilio_auth_token',
            'yatoon_whatsapp_access_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_client_secret', 'yatoon_vagaro_region', 'yatoon_vagaro_business_id', 'yatoon_vagaro_timezone',
            '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_waitlist_claim_minutes',
            '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_portal_signin_label', 'yatoon_portal_signin_bg',
            'yatoon_portal_signin_text', 'yatoon_portal_signin_panel_bg',
            'yatoon_portal_email_signin_label', 'yatoon_portal_sms_signin_label',
            'yatoon_portal_email_login_enabled', 'yatoon_portal_sms_login_enabled',
            'yatoon_portal_change_service_enabled',
            'yatoon_show_ratings_on_cards', 'yatoon_public_rating_label',
            // 4.4.3: tax / VAT and invoicing.
            'yatoon_tax_enabled', 'yatoon_tax_rate', 'yatoon_tax_label',
            'yatoon_tax_prices_include', 'yatoon_tax_registration_id',
            'yatoon_invoice_enabled', 'yatoon_invoice_prefix',
            'yatoon_currency',
            '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_ai_reschedule_assistant_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_waitlist_claim_minutes',
            '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_portal_email_login_enabled', 'yatoon_portal_sms_login_enabled',
            'yatoon_portal_change_service_enabled',
            '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_ai_reschedule_assistant_enabled', 'yatoon_pwa_enabled', 'yatoon_push_relay_enabled',
            'yatoon_cof_enabled', 'yatoon_cof_require', 'yatoon_cof_auto_charge', 'yatoon_cof_noshow_mode',
            'yatoon_cof_noshow_amount', 'yatoon_cof_late_cancel_mode', 'yatoon_cof_late_cancel_amount',
            'yatoon_cof_late_cancel_hours', 'yatoon_cof_policy_text',
            'yatoon_show_advanced_menu', 'yatoon_test_booking_mode',
            // 4.4.3
            'yatoon_tax_enabled', 'yatoon_tax_prices_include', 'yatoon_invoice_enabled',
        ) );
        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', 'yatoon-booking-system' ) );
        }
        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' );
        update_option( 'yatoon_ai_reschedule_assistant_enabled', ! empty( $_POST['yatoon_ai_reschedule_assistant_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 : __( 'Not sure what to book?', '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 : __( 'Please do not include medical, payment, or other sensitive information.', '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' );
        $frontend_profile = isset( $_POST['yatoon_ai_frontend_profile'] ) ? sanitize_key( wp_unslash( $_POST['yatoon_ai_frontend_profile'] ) ) : 'fast';
        update_option( 'yatoon_ai_frontend_profile', in_array( $frontend_profile, array( 'fast', 'balanced', 'quality' ), true ) ? $frontend_profile : 'fast' );
        $cache_minutes = isset( $_POST['yatoon_ai_cache_minutes'] ) ? absint( wp_unslash( $_POST['yatoon_ai_cache_minutes'] ) ) : 30;
        update_option( 'yatoon_ai_cache_minutes', min( 120, max( 5, $cache_minutes ) ) );

        $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-3.7-flash', 'gemini-3.6-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash', 'gemini-1.5-flash-8b' ),
            'openai'    => array( 'gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o-mini' ),
            'anthropic' => array( 'claude-haiku-4-5', 'claude-sonnet-5', 'claude-opus-5', 'claude-fable-5', 'claude-3-5-haiku-20241022', 'claude-3-haiku-20240307', 'claude-3-5-sonnet-20241022', 'claude-3-7-sonnet-20250219' ),
        );
        $model_defaults = array(
            'google'    => 'gemini-2.0-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_whatsapp_cloud_enabled', 'yatoon_whatsapp_phone_number_id',
            'yatoon_whatsapp_access_token', 'yatoon_whatsapp_api_version',
            'yatoon_whatsapp_language', 'yatoon_whatsapp_customer_confirmation',
            'yatoon_whatsapp_confirmation_template', 'yatoon_whatsapp_admin_alert',
            'yatoon_whatsapp_admin_number', 'yatoon_whatsapp_admin_template',
            'yatoon_whatsapp_reminder_enabled', 'yatoon_whatsapp_reminder_template',
            'yatoon_whatsapp_customer_updates', 'yatoon_whatsapp_cancel_template',
            'yatoon_whatsapp_reschedule_template', 'yatoon_whatsapp_test_template',
        ), 'yatoon_save_sms_settings', array(
            'yatoon_twilio_enabled', 'yatoon_twilio_customer_sms',
            'yatoon_twilio_admin_sms', 'yatoon_twilio_reminder_sms', 'yatoon_twilio_logs',
            'yatoon_whatsapp_cloud_enabled', 'yatoon_whatsapp_customer_confirmation',
            'yatoon_whatsapp_admin_alert', 'yatoon_whatsapp_reminder_enabled',
            'yatoon_whatsapp_customer_updates',
        ) );
    }

    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',
            // 4.4.3: two-way sync.
            'yatoon_google_cal_two_way', 'yatoon_google_cal_two_way_shared',
        ), 'yatoon_save_calendar_settings', array(
            'yatoon_google_cal_enabled', 'yatoon_google_cal_send_invites',
            'yatoon_google_cal_two_way', 'yatoon_google_cal_two_way_shared',
        ) );

        // Per-staff calendar IDs arrive as an array, which save_options() does
        // not handle, so store them separately.
        if ( isset( $_POST['yatoon_google_cal_staff_map'] ) && is_array( $_POST['yatoon_google_cal_staff_map'] ) ) {
            check_admin_referer( 'yatoon_save_calendar_settings' );
            if ( ! current_user_can( 'manage_options' ) ) {
                return;
            }
            $map = array();
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each value sanitized below.
            foreach ( wp_unslash( $_POST['yatoon_google_cal_staff_map'] ) as $staff_id => $calendar_id ) {
                $calendar_id = sanitize_text_field( (string) $calendar_id );
                if ( '' !== trim( $calendar_id ) ) {
                    $map[ absint( $staff_id ) ] = $calendar_id;
                }
            }
            update_option( 'yatoon_google_cal_staff_map', $map, false );
            if ( class_exists( 'YATOON_Google_Calendar' ) ) {
                YATOON_Google_Calendar::flush_busy_cache();
            }
        }
    }

    public function handle_save_integrations() {
        check_admin_referer( 'yatoon_save_integrations_settings' );
        if ( ! current_user_can( 'manage_options' ) ) { wp_die( __( 'Forbidden', 'yatoon-booking-system' ) );
        }
        $checkboxes = array( 'yatoon_woocommerce_enabled', 'yatoon_woocommerce_email_payment_link', 'yatoon_webhooks_enabled', 'yatoon_webhook_include_customer', 'yatoon_zoom_enabled', 'yatoon_zoom_email_link', 'yatoon_google_meet_enabled', 'yatoon_google_meet_email_link' );
        foreach ( $checkboxes as $key ) { update_option( $key, isset( $_POST[ $key ] ) ? '1' : '0' );
        }
        $text = array( 'yatoon_zoom_account_id', 'yatoon_zoom_client_id', 'yatoon_zoom_host_user' );
        foreach ( $text as $key ) { update_option( $key, isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '' );
        }
        $gateway = isset( $_POST['yatoon_deposit_gateway'] ) ? sanitize_key( wp_unslash( $_POST['yatoon_deposit_gateway'] ) ) : 'stripe';
        update_option( 'yatoon_deposit_gateway', in_array( $gateway, array( 'stripe', 'paypal', 'woocommerce' ), true ) ? $gateway : 'stripe' );
        $hold_minutes = isset( $_POST['yatoon_woocommerce_hold_minutes'] ) ? absint( $_POST['yatoon_woocommerce_hold_minutes'] ) : 30;
        update_option( 'yatoon_woocommerce_hold_minutes', min( 120, max( 5, $hold_minutes ) ) );
        $webhook_url = isset( $_POST['yatoon_webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['yatoon_webhook_url'] ) ) : '';
        update_option( 'yatoon_webhook_url', $webhook_url );
        foreach ( array( 'yatoon_webhook_secret', 'yatoon_zoom_client_secret' ) as $key ) {
            $value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
            if ( '' !== $value ) { YATOON_Crypto::update_option( $key, $value );
            }
        }
        $service_fields = array( 'yatoon_zoom_service_ids', 'yatoon_google_meet_service_ids' );
        foreach ( $service_fields as $field ) {
            $values = isset( $_POST[ $field ] ) ? (array) wp_unslash( $_POST[ $field ] ) : array();
            update_option( $field, array_values( array_unique( array_filter( array_map( 'absint', $values ) ) ) ), false );
        }
        $allowed_events = array( 'booking.created', 'booking.cancelled', 'booking.rescheduled', 'booking.no_show' );
        $events = isset( $_POST['yatoon_webhook_events'] ) ? (array) wp_unslash( $_POST['yatoon_webhook_events'] ) : array();
        $events = array_values( array_intersect( $allowed_events, array_map( 'sanitize_text_field', $events ) ) );
        update_option( 'yatoon_webhook_events', $events, false );
        delete_transient( 'yatoon_zoom_access_token' );
        wp_safe_redirect( add_query_arg( array( 'page' => 'yatoon-booking-system', 'tab' => 'integrations', 'saved' => '1' ), admin_url( 'admin.php' ) ) );
        exit;
    }

    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',
            // Button Labels, Fonts & Colours panel (10 buttons) - these were
            // registered via register_setting() but never actually added to
            // this whitelist, so every field below was silently discarded on
            // save regardless of what the merchant typed in - not a caching
            // issue, not a CSS cascade issue, the value never reached the
            // database in the first place.
            'yatoon_add_btn_label', 'yatoon_add_btn_font', 'yatoon_add_btn_size',
            'yatoon_add_btn_text_color', 'yatoon_add_btn_bg_color', 'yatoon_add_btn_radius',
            'yatoon_back_btn_label', 'yatoon_back_btn_font', 'yatoon_back_btn_size',
            'yatoon_back_btn_text_color', 'yatoon_back_btn_bg_color', 'yatoon_back_btn_radius',
            'sbs_continue_btn_label', 'sbs_continue_btn_font', 'sbs_continue_btn_size',
            'sbs_continue_btn_text_color', 'sbs_continue_btn_bg_color', 'sbs_continue_btn_radius',
            'sbs_addmore_btn_label', 'sbs_addmore_btn_font', 'sbs_addmore_btn_size',
            'sbs_addmore_btn_text_color', 'sbs_addmore_btn_bg_color', 'sbs_addmore_btn_radius',
            'sbs_addmore2_btn_label', 'sbs_addmore2_btn_font', 'sbs_addmore2_btn_size',
            'sbs_addmore2_btn_text_color', 'sbs_addmore2_btn_bg_color', 'sbs_addmore2_btn_radius',
            'sbs_addmore3_btn_label', 'sbs_addmore3_btn_font', 'sbs_addmore3_btn_size',
            'sbs_addmore3_btn_text_color', 'sbs_addmore3_btn_bg_color', 'sbs_addmore3_btn_radius',
            'sbs_add_for_person_btn_label', 'sbs_add_for_person_btn_size',
            'sbs_add_for_person_btn_text_color', 'sbs_add_for_person_btn_bg_color', 'sbs_add_for_person_btn_radius',
            'sbs_add_guest_btn_label', 'sbs_add_guest_btn_size',
            'sbs_add_guest_btn_text_color', 'sbs_add_guest_btn_bg_color', 'sbs_add_guest_btn_radius',
            'sbs_confirm_btn_label', 'sbs_confirm_btn_font', 'sbs_confirm_btn_size',
            'sbs_confirm_btn_text_color', 'sbs_confirm_btn_bg_color', 'sbs_confirm_btn_radius',
            'sbs_confirm_single_btn_label', 'sbs_confirm_single_btn_size',
            'sbs_confirm_single_btn_text_color', 'sbs_confirm_single_btn_bg_color', 'sbs_confirm_single_btn_radius',
        ), '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')));
        // 4.6.1: the push relay sends data to a Cloudflare Worker operated by
        // the plugin author. It stays off until the site owner opts in.
        register_setting('yatoon_settings', 'yatoon_push_relay_enabled', array('sanitize_callback' => array($this, 'sanitize_setting')));

        // 4.6.1 card on file + cancellation policy fees.
        foreach ( array(
            'yatoon_cof_enabled',
            'yatoon_cof_require',
            'yatoon_cof_auto_charge',
            'yatoon_cof_noshow_mode',
            'yatoon_cof_late_cancel_mode',
        ) as $yatoon_cof_key ) {
            register_setting( 'yatoon_settings', $yatoon_cof_key, array( 'sanitize_callback' => array( $this, 'sanitize_setting' ) ) );
        }
        register_setting( 'yatoon_settings', 'yatoon_cof_noshow_amount', array( 'sanitize_callback' => 'floatval' ) );
        register_setting( 'yatoon_settings', 'yatoon_cof_late_cancel_amount', array( 'sanitize_callback' => 'floatval' ) );
        register_setting( 'yatoon_settings', 'yatoon_cof_late_cancel_hours', array( 'sanitize_callback' => 'absint' ) );
        register_setting( 'yatoon_settings', 'yatoon_cof_policy_text', array( 'sanitize_callback' => 'sanitize_textarea_field' ) );
        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_portal_signin_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_email_signin_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_sms_signin_label', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_portal_signin_bg', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_settings', 'yatoon_portal_signin_text', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_settings', 'yatoon_portal_signin_panel_bg', array('sanitize_callback' => 'sanitize_hex_color'));
        register_setting('yatoon_settings', 'yatoon_portal_email_login_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_portal_sms_login_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('yatoon_settings', 'yatoon_portal_change_service_enabled', array('sanitize_callback' => 'sanitize_text_field'));
        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_client_secret', array('sanitize_callback' => array($this, 'sanitize_setting')));
        register_setting('yatoon_settings', 'yatoon_vagaro_region', 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'));
        register_setting('yatoon_settings', 'yatoon_waitlist_claim_minutes', array('sanitize_callback' => 'absint'));

        // 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);
        $settings_checkbox_css = <<<'CSS'
.yatoon-settings-page input[type="checkbox"]{-webkit-appearance:none!important;appearance:none!important;display:inline-grid!important;place-items:center!important;width:16px!important;height:16px!important;min-width:16px!important;margin:0!important;margin-inline-end:7px!important;padding:0!important;vertical-align:-3px!important;border:1px solid #8c8f94!important;border-radius:3px!important;background:#fff!important;box-shadow:none!important;transition:none!important;cursor:pointer}
.yatoon-settings-page input[type="checkbox"]:checked{border-color:#3858e9!important;background:#3858e9!important}
.yatoon-settings-page input[type="checkbox"]:checked::before{content:""!important;display:block!important;width:4px!important;height:8px!important;margin:-1px 0 0!important;border:solid #fff!important;border-width:0 2px 2px 0!important;transform:rotate(45deg)!important}
.yatoon-settings-page input[type="checkbox"]:focus-visible{outline:2px solid #3858e9!important;outline-offset:2px!important}
.yatoon-settings-page input[type="checkbox"]:disabled{opacity:.5!important;cursor:not-allowed}
CSS;
        wp_add_inline_style('yatoon-design-system', $settings_checkbox_css);
        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);

        // 4.6.1 V4: per-screen admin stylesheets.
        //
        // These used to be <style> blocks printed inside the view templates,
        // so a salon opening the Services screen re-downloaded 15 KB of CSS
        // every single time. As files they are cached, minified, RTL-flipped
        // and checked by the asset gate like every other stylesheet.
        $screen_styles = array(
            'sbs-services'       => 'services',
            'yatoon-calendar'    => 'calendar',
            'yatoon-dashboard'   => 'dashboard',
            'yatoon-clients'     => 'clients',
            'yatoon-setup-wizard'=> 'setup-wizard',
            'sbs-staff'          => 'staff',
        );
        if ( isset( $screen_styles[ $page_slug ] ) ) {
            $screen_css = $screen_styles[ $page_slug ];
            $css_rtl    = is_rtl() ? '-rtl' : '';
            $css_rel    = 'assets/css/admin-' . $screen_css . YATOON_ASSET_SUFFIX . $css_rtl . '.css';
            if ( ! file_exists( SBS_PLUGIN_DIR . $css_rel ) ) {
                $css_rel = 'assets/css/admin-' . $screen_css . '.css';
            }
            if ( file_exists( SBS_PLUGIN_DIR . $css_rel ) ) {
                wp_enqueue_style(
                    'yatoon-admin-' . $screen_css,
                    SBS_PLUGIN_URL . $css_rel,
                    // After the design system, so these screen rules keep
                    // winning the cascade fights they won when they were the
                    // last thing on the page.
                    array( 'yatoon-design-system' ),
                    SBS_VERSION . '.' . filemtime( SBS_PLUGIN_DIR . $css_rel )
                );
            }
        }

        // 4.6.1 V2: per-screen admin scripts.
        //
        // These three files used to be ~2,100 lines of inline <script> inside
        // admin/views/{services,bookings,settings}.php. Inline admin JS is
        // never cached, never minified, and invisible to linting -- and it is
        // why an audit could count 74 wp_ajax actions with "no JavaScript
        // caller" when the callers existed all along, buried in PHP.
        //
        // Nonces move across through wp_localize_script rather than being
        // interpolated into the script body.
        $screen_scripts = array(
            'sbs-services'          => 'services',
            'sbs-bookings'          => 'bookings',
            'sbs-staff'             => 'staff',
            'yatoon-calendar'       => 'calendar',
            'yatoon-clients'        => 'clients',
            // admin/views/settings.php is rendered by six different menu
            // pages, not one: the Pro sub-pages include it with $_GET['tab']
            // forced. Listing only the main slug here is exactly how the
            // settings script would silently fail to load on five screens
            // whose markup still expects it.
            'yatoon-booking-system' => 'settings',
            'sbs-sms-pro'           => 'settings',
            'sbs-gcal-pro'          => 'settings',
            'yatoon-deposit-pro'    => 'settings',
            'yatoon-noshow-pro'     => 'settings',
            'yatoon-waitlist-pro'   => 'settings',
        );
        // Calendar and Clients appear in the map but get no wp_localize_script
        // here: their data is only known once the view has run its queries, so
        // they localize from inside the view. The script is in the footer, so
        // that still lands before it executes -- the same pattern Reports uses.
        if ( isset( $screen_scripts[ $page_slug ] ) ) {
            $screen  = $screen_scripts[ $page_slug ];
            $rel     = 'assets/js/admin-' . $screen . YATOON_ASSET_SUFFIX . '.js';
            if ( ! file_exists( SBS_PLUGIN_DIR . $rel ) ) {
                $rel = 'assets/js/admin-' . $screen . '.js';
            }
            $handle  = 'yatoon-admin-' . $screen;
            wp_enqueue_script(
                $handle,
                SBS_PLUGIN_URL . $rel,
                array( 'jquery', 'sbs-admin-js' ),
                SBS_VERSION . '.' . ( file_exists( SBS_PLUGIN_DIR . $rel ) ? filemtime( SBS_PLUGIN_DIR . $rel ) : '0' ),
                true
            );

            // 4.6.2: none of the admin screen scripts were ever registered for
            // script translations, so every wp.i18n __() call in them returned
            // the English source no matter how complete the .po catalogues were
            // -- the strings were correctly wrapped and completely inert. One
            // call here covers every screen that goes through this enqueue.
            wp_set_script_translations( $handle, 'yatoon-booking-system', SBS_PLUGIN_DIR . 'languages' );

            if ( 'services' === $screen ) {
                global $wpdb;
                $categories = $wpdb->get_col( "SELECT DISTINCT category FROM {$wpdb->prefix}yatoon_services WHERE category IS NOT NULL AND category <> '' ORDER BY category ASC" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                wp_localize_script( $handle, 'yatoonServicesData', array(
                    'categories'    => array_values( array_map( 'strval', (array) $categories ) ),
                    'categoryNonce' => wp_create_nonce( 'yatoon_service_category' ),
                    'serviceNonce'  => wp_create_nonce( 'yatoon_service' ),
                    'optionNonce'   => wp_create_nonce( 'yatoon_option' ),
                    'orderNonce'    => wp_create_nonce( 'yatoon_service_order' ),
                    'adminNonce'    => wp_create_nonce( 'yatoon_admin_nonce' ),
                    'aiDescNonce'   => wp_create_nonce( 'yatoon_ai_service_description' ),
                ) );
            } elseif ( 'staff' === $screen ) {
                wp_localize_script( $handle, 'yatoonStaffData', array(
                    'adminNonce'          => wp_create_nonce( 'yatoon_admin_nonce' ),
                    'staffServicesNonce'  => wp_create_nonce( 'yatoon_staff_services' ),
                    'toggleNonce'         => wp_create_nonce( 'yatoon_toggle_staff' ),
                    'pinNonce'            => wp_create_nonce( 'yatoon_set_staff_pin' ),
                    'pinLength'           => in_array( (int) get_option( 'yatoon_staff_pin_length', 6 ), array( 4, 6 ), true )
                        ? (int) get_option( 'yatoon_staff_pin_length', 6 )
                        : 6,
                ) );
            } elseif ( 'bookings' === $screen ) {
                wp_localize_script( $handle, 'yatoonBookingsData', array(
                    'adminNonce'  => wp_create_nonce( 'yatoon_admin_nonce' ),
                    'deleteNonce' => wp_create_nonce( 'yatoon_delete_bookings' ),
                    'exportNonce' => wp_create_nonce( 'yatoon_export_bookings' ),
                    'cancelNonce' => wp_create_nonce( 'yatoon_cancel_booking' ),
                ) );
            } elseif ( 'settings' === $screen ) {
                wp_localize_script( $handle, 'yatoonSettingsData', array(
                    'ajaxUrl'      => admin_url( 'admin-ajax.php' ),
                    'adminNonce'   => wp_create_nonce( 'yatoon_admin_nonce' ),
                    'squareNonce'  => wp_create_nonce( 'yatoon_save_square_api' ),
                    'restoreNonce' => wp_create_nonce( 'yatoon_restore_defaults' ),
                ) );
            }
        }
        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');
        }

        // Reports page: vendored Chart.js (no CDN) for the revenue trend and
        // revenue-by-service/staff charts. The actual chart data is localized
        // from admin/views/reports.php once it computes the report numbers.
        if ( 'yatoon-reports' === $page_slug ) {
            wp_enqueue_script( 'yatoon-chartjs', SBS_PLUGIN_URL . 'assets/js/vendor/chart.umd.min.js', array(), '4.5.1', true );
            wp_enqueue_script( 'yatoon-reports-charts', SBS_PLUGIN_URL . 'assets/js/yatoon-reports-charts' . YATOON_ASSET_SUFFIX . '.js', array( 'yatoon-chartjs' ), $admin_asset_version, true );
        }

        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(),
            'whatsapp_enabled' => class_exists('YATOON_WhatsApp') && YATOON_WhatsApp::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', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }

        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 ) {
            // Look for any existing page that already carries this shortcode
            // before creating a new one. Without this, running the wizard's
            // "create pages" step -- "Also create customer self-service
            // pages" is checked by default -- on a site that already has one
            // (e.g. the customer portal page YATOON_Activator::
            // ensure_customer_portal_page() creates unconditionally on every
            // activation, before anyone even opens the wizard) silently
            // creates a duplicate "My Appointments" page and re-points every
            // customer-facing portal link at the new one via yatoon_portal_url,
            // orphaning the original. Mirrors the same DB search
            // ensure_customer_portal_page() already does for the same reason.
            global $wpdb;
            $bare = trim( $shortcode, '[]' );
            $like = '%' . $wpdb->esc_like( '[' . $bare ) . '%';
            $found_id = absint( $wpdb->get_var( $wpdb->prepare(
                "SELECT ID FROM {$wpdb->posts} WHERE post_type='page' AND post_status NOT IN ('trash','auto-draft') AND post_content LIKE %s ORDER BY ID ASC LIMIT 1",
                $like
            ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            if ( $found_id ) {
                $page_id = $found_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'] );
        $open_import = false;
        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 ) {
            $import_choice = sanitize_key( wp_unslash( $_POST['yatoon_import_choice'] ?? 'fresh' ) );
            $import_choice = in_array( $import_choice, array( 'fresh', 'import' ), true ) ? $import_choice : 'fresh';
            update_option( 'yatoon_onboarding_import_choice', $import_choice );
            $open_import = 'import' === $import_choice;
            $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 );
            // Fresh installs start lean. When the wizard selects Square,
            // advance the untouched Local profile to the tested Square
            // single-store profile. Upgraded/custom sites are never changed.
            if ( class_exists( 'YATOON_Feature_Modules' )
                && 'square' === $platform
                && 'local' === YATOON_Feature_Modules::current_profile()
                && YATOON_VERSION === (string) get_option( YATOON_Feature_Modules::PROFILE_VERSION_OPTION, '' )
            ) {
                YATOON_Feature_Modules::apply_profile( 'square_single', true );
            }
            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'] ) ) );
                }
                if ( !empty( $_POST['yatoon_vagaro_client_secret'] ) ) { YATOON_Crypto::update_option( 'yatoon_vagaro_client_secret', sanitize_text_field( wp_unslash( $_POST['yatoon_vagaro_client_secret'] ) ) );
                }
                update_option( 'yatoon_vagaro_region', strtolower( sanitize_key( wp_unslash( $_POST['yatoon_vagaro_region'] ?? '' ) ) ) );
                update_option( 'yatoon_vagaro_business_id', sanitize_text_field( wp_unslash( $_POST['yatoon_vagaro_business_id'] ?? '' ) ) );
            }
            if ( !$open_import && 'demo' === sanitize_key( wp_unslash( $_POST['yatoon_starter_mode'] ?? 'blank' ) ) ) {
                YATOON_Activator::seed_starter_data( sanitize_key( wp_unslash( $_POST['yatoon_starter_industry'] ?? 'nail' ) ) );
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 ) );
            }
        }
        if ( $open_import ) {
            wp_safe_redirect( admin_url( 'admin.php?page=yatoon-import&from=wizard' ) );
            exit;
        }
        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_Vagaro_API::is_configured();
        }
        $notifications_ready = get_option('yatoon_send_confirmation_email', '1') === '1'
            || get_option('yatoon_twilio_enabled', '0') === '1'
            || get_option('yatoon_whatsapp_cloud_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, SMS, and WhatsApp are 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
        // 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     = class_exists( 'YATOON_License' ) ? YATOON_License::free_limit( 'staff' ) : 2;
        // 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
        // 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;
        }

        if ( class_exists( 'YATOON_Staff_Portal' ) && method_exists( 'YATOON_Staff_Portal', 'maybe_auto_sync_bookings' ) ) {
            YATOON_Staff_Portal::maybe_auto_sync_bookings();
        }

        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 );
            } ) );
        }

        // Use the exact same time-range approach the Staff Portal uses (a
        // precise start/end timestamp bound in the site's timezone) instead
        // of comparing with MySQL's DATE() function, so this page can never
        // show a different set of bookings for the same day than the Staff
        // Portal does for the same staff member.
        $tz_obj      = yatoon_timezone();
        $range_start = new DateTime( $date . ' 00:00:00', $tz_obj );
        $range_end   = new DateTime( $date . ' 23:59:59', $tz_obj );
        $start_dt    = $range_start->format( 'Y-m-d H:i:s' );
        $end_dt      = $range_end->format( 'Y-m-d H:i:s' );

        $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 b.start_time >= %s
             AND 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",
            $start_dt, $end_dt
        ));

        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
        ) );

        $advanced_metrics = class_exists( 'YATOON_Reporting' ) ? YATOON_Reporting::collect( $date_from, $date_to ) : array();

        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
        // 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;
        }
        // The Notifications screen is always registered, but
        // class-yatoon-notifications.php only loads when the "notifications"
        // module is enabled. Load it on demand so opening the settings screen
        // with the module switched off cannot fatal.
        if ( ! class_exists( 'YATOON_Notifications' ) ) {
            YATOON_Feature_Modules::ensure_loaded( 'notifications' );
        }
        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
        // 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', 'yatoon-booking-system' )));
        }
        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) { wp_send_json_error(array('message' => __( 'Gift Cards', 'yatoon-booking-system' )));
        }

        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', 'yatoon-booking-system' )));
        }

        // 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.', 'yatoon-booking-system' )));
            }
            $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', 'yatoon-booking-system' );
        $exp_line  = $expires ? '<p style="font-size:13px;color:#6b7280;">' . esc_html__( 'Expires:', 'yatoon-booking-system' ) . ' ' . esc_html( 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;">' . esc_html__( 'You received a gift card!', 'yatoon-booking-system' ) . '</h2>
    <p style="font-size:15px;color:#374151;margin:0 0 24px;">' . sprintf( esc_html__( 'Hi %1$s, you have received a gift card for %2$s!', 'yatoon-booking-system' ), esc_html($first), esc_html($business) ) . '</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;">' . esc_html__( 'YOUR GIFT CARD CODE', 'yatoon-booking-system' ) . '</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;">' . esc_html__( 'Enter this code at checkout when booking your appointment online.', 'yatoon-booking-system' ) . '</p>
    <p style="font-size:12px;color:#9ca3af;border-top:1px solid #f3f4f6;padding-top:16px;margin-top:24px;">' . sprintf( esc_html__( 'This gift card was issued by %s.', 'yatoon-booking-system' ), esc_html($business) ) . '</p>
</div>';

        wp_mail($rec_email, sprintf( __( 'You have a gift card for %s', 'yatoon-booking-system' ), $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', 'yatoon-booking-system' )));
        }
        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', 'yatoon-booking-system' )));
        }
        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 ( function_exists( 'yatoon_public_rate_limit' ) ) {
            yatoon_public_rate_limit( 'gift_card_validate', 8, 10 );
        }
        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) { wp_send_json_error(array('message' => __( 'Gift Cards', 'yatoon-booking-system' )));
        }
        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));

        // Keep invalid-card responses indistinguishable so this endpoint cannot be used as a card-number oracle.
        if ( ! $gc || 'active' !== $gc->status || ( $gc->expires_at && strtotime( $gc->expires_at ) < time() ) || floatval( $gc->balance ) <= 0 ) {
            wp_send_json_error( array( 'message' => __( 'This gift card could not be applied. Check the code or contact the business.', 'yatoon-booking-system' ) ) );
        }

        wp_send_json_success(array(
            'balance' => floatval($gc->balance),
            'code'    => $gc->code,
            'message' => sprintf(
                /* translators: %s: remaining gift card balance, already formatted. */
                __('Gift card valid! Balance: %s', 'yatoon-booking-system'),
                '$' . 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', 'yatoon-booking-system' ), __( 'Sell pre-paid session bundles (e.g. Buy 5 manicures, get 1 free). Clients redeem sessions at booking. Requires an active Pro license.', 'yatoon-booking-system' ) );
            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', 'yatoon-booking-system' )));
        }
        $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', 'yatoon-booking-system' )));
        }
        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', 'yatoon-booking-system' )));
        }
        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>' . esc_html__( 'Saved!', 'yatoon-booking-system' ) . '</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.', 'yatoon-booking-system' ));
        include SBS_PLUGIN_DIR . 'admin/views/closed-dates.php';
    }

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

        $proxy_errors = array();

        // 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'] ?? ''));
            $new_proxy_enabled = isset($_POST['yatoon_trusted_proxy_enabled']);
            $new_proxy_header = sanitize_key(wp_unslash($_POST['yatoon_trusted_proxy_header'] ?? 'x_forwarded_for'));
            $new_proxy_ranges = sanitize_textarea_field(wp_unslash($_POST['yatoon_trusted_proxy_ranges'] ?? ''));
            $proxy_validation = YATOON_Request_Guard::validate_proxy_ranges($new_proxy_ranges);
            if (!in_array($new_proxy_header, array('auto', 'cf_connecting_ip', 'x_forwarded_for', 'x_real_ip'), true)) {
                $proxy_errors[] = $new_proxy_header;
            }
            if ($new_proxy_enabled && empty($proxy_validation['valid'])) {
                $proxy_errors[] = __('At least one valid trusted proxy IP or CIDR is required when trusted proxy mode is enabled.', 'yatoon-booking-system');
            }
            $proxy_errors = array_values(array_unique(array_merge($proxy_errors, $proxy_validation['invalid'])));
            if (empty($proxy_errors)) {
                update_option('yatoon_trusted_proxy_enabled', $new_proxy_enabled ? '1' : '0');
                update_option('yatoon_trusted_proxy_header', $new_proxy_header);
                update_option('yatoon_trusted_proxy_ranges', implode("\n", $proxy_validation['valid']));
            }
            update_option('yatoon_delete_data_on_uninstall', isset($_POST['yatoon_delete_data_on_uninstall']) ? '1' : '0');
            if (!empty($_POST['yatoon_turnstile_secret_key'])) {
                YATOON_Crypto::update_option('yatoon_turnstile_secret_key', sanitize_text_field(wp_unslash($_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');
            if ($proxy_errors) {
                echo '<div class="notice notice-error"><p>' . esc_html(sprintf(__('Other protection settings were saved, but trusted proxy settings were not changed. Invalid entries: %s', 'yatoon-booking-system'), implode(', ', $proxy_errors))) . '</p></div>';
            } else {
                echo '<div class="notice notice-success"><p>' . esc_html__('Blacklist and protection settings saved.', 'yatoon-booking-system') . '</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', '');
        $trusted_proxy_enabled = get_option('yatoon_trusted_proxy_enabled', '0') === '1';
        $trusted_proxy_header  = sanitize_key(get_option('yatoon_trusted_proxy_header', 'x_forwarded_for'));
        $trusted_proxy_ranges  = get_option('yatoon_trusted_proxy_ranges', '');
        $recognized_client_ip  = YATOON_Request_Guard::client_ip();
        $remote_peer_ip        = isset($_SERVER['REMOTE_ADDR']) && filter_var(wp_unslash($_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
        $delete_data_on_uninstall = get_option('yatoon_delete_data_on_uninstall', '0') === '1';
        $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><?php esc_html_e( 'Booking Protection & Blacklist', 'yatoon-booking-system' ); ?></h1>
            <p style="color:#555;max-width:700px;"><?php esc_html_e( 'Block abusive customers and limit excessive bookings. All checks run silently - blocked visitors see a generic "please call us" message.', 'yatoon-booking-system' ); ?></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><?php esc_html_e( 'Max upcoming bookings', 'yatoon-booking-system' ); ?><br><small style="font-weight:normal;color:#777;"><?php esc_html_e( 'per phone number', 'yatoon-booking-system' ); ?></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"><?php echo wp_kses_post( __( '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.', 'yatoon-booking-system' ) ); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">
                            <label><?php esc_html_e( 'IP rate limit', 'yatoon-booking-system' ); ?><br><small style="font-weight:normal;color:#777;"><?php esc_html_e( 'max attempts per window', 'yatoon-booking-system' ); ?></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;"><?php esc_html_e( 'attempts within', 'yatoon-booking-system' ); ?></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;"><?php esc_html_e( 'minutes', 'yatoon-booking-system' ); ?></span>
                            <p class="description"><?php echo wp_kses_post( __( '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.', 'yatoon-booking-system' ) ); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label><?php esc_html_e( 'Blocked phone numbers', 'yatoon-booking-system' ); ?></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"><?php echo wp_kses_post( __( 'One number per line. Any format: <code>3105550000</code>, <code>(310) 555-0000</code>, <code>+1-310-555-0000</code>', 'yatoon-booking-system' ) ); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label><?php esc_html_e( 'Blocked email addresses', 'yatoon-booking-system' ); ?></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"><?php esc_html_e( 'One email per line. Case-insensitive.', 'yatoon-booking-system' ); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label><?php esc_html_e( 'Blocked IP addresses', 'yatoon-booking-system' ); ?></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"><?php esc_html_e( 'One IP per line (IPv4). Use sparingly - IPs can change. Best for repeated bot attacks.', 'yatoon-booking-system' ); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label><?php esc_html_e( 'Minimum time to submit', 'yatoon-booking-system' ); ?></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;"> <?php esc_html_e( 'seconds', 'yatoon-booking-system' ); ?>
                            <p class="description"><?php echo wp_kses_post( __( '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.', 'yatoon-booking-system' ) ); ?></p>
                        </td>
                    </tr>
                </table>

                <h2 style="margin-top:28px;"><?php esc_html_e('Trusted proxy', 'yatoon-booking-system'); ?></h2>
                <p class="description"><?php esc_html_e('Only enable this when WordPress is behind a proxy you control. Forwarded IP headers are ignored by default.', 'yatoon-booking-system'); ?></p>
                <p><strong><?php esc_html_e('Current recognized visitor IP:', 'yatoon-booking-system'); ?></strong> <code><?php echo esc_html($recognized_client_ip ?: __('Unavailable', 'yatoon-booking-system')); ?></code><?php if ($remote_peer_ip && $remote_peer_ip !== $recognized_client_ip) : ?> <span class="description"><?php echo esc_html(sprintf(__('Direct connection peer: %s', 'yatoon-booking-system'), $remote_peer_ip)); ?></span><?php endif; ?></p>
                <table class="form-table" style="max-width:800px;">
                    <tr><th scope="row"><?php esc_html_e('Enable', 'yatoon-booking-system'); ?></th><td><label><input type="checkbox" name="yatoon_trusted_proxy_enabled" value="1" <?php checked($trusted_proxy_enabled, true); ?>> <?php esc_html_e('Use forwarded client IPs only from the trusted proxy ranges below', 'yatoon-booking-system'); ?></label></td></tr>
                    <tr><th scope="row"><label for="yatoon_trusted_proxy_header"><?php esc_html_e('Client IP header', 'yatoon-booking-system'); ?></label></th><td><select id="yatoon_trusted_proxy_header" name="yatoon_trusted_proxy_header"><option value="auto" <?php selected($trusted_proxy_header, 'auto'); ?>><?php esc_html_e('Automatic', 'yatoon-booking-system'); ?></option><option value="cf_connecting_ip" <?php selected($trusted_proxy_header, 'cf_connecting_ip'); ?>>CF-Connecting-IP</option><option value="x_forwarded_for" <?php selected($trusted_proxy_header, 'x_forwarded_for'); ?>>X-Forwarded-For</option><option value="x_real_ip" <?php selected($trusted_proxy_header, 'x_real_ip'); ?>>X-Real-IP</option></select></td></tr>
                    <tr><th scope="row"><label for="yatoon_trusted_proxy_ranges"><?php esc_html_e('Trusted proxy IPs/CIDRs', 'yatoon-booking-system'); ?></label></th><td><textarea id="yatoon_trusted_proxy_ranges" name="yatoon_trusted_proxy_ranges" rows="4" class="large-text code"><?php echo esc_textarea($trusted_proxy_ranges); ?></textarea><p class="description"><?php esc_html_e('One IPv4, IPv6, or CIDR range per line. Never enter a public catch-all range.', 'yatoon-booking-system'); ?></p></td></tr>
                </table>

                <h2 style="margin-top:28px;"><?php esc_html_e('Uninstall data policy', 'yatoon-booking-system'); ?></h2>
                <p><label><input type="checkbox" name="yatoon_delete_data_on_uninstall" value="1" <?php checked($delete_data_on_uninstall, true); ?>> <strong><?php esc_html_e('Permanently delete all Yatoon business data when the plugin is uninstalled', 'yatoon-booking-system'); ?></strong></label></p>
                <p class="description"><?php esc_html_e('Off by default. Leave this unchecked to preserve appointments, customers, staff, services, settings, and integration data.', 'yatoon-booking-system'); ?></p>

                <h2 style="margin-top:28px;"><?php esc_html_e( 'Invisible bot check (Cloudflare Turnstile)', 'yatoon-booking-system' ); ?></h2>
                <?php // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Administrator setup link; no plugin asset is offloaded. ?>
                <p style="color:#555;max-width:700px;"><?php esc_html_e( '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.', 'yatoon-booking-system' ); ?> <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank"><?php esc_html_e( 'Get free keys from Cloudflare', 'yatoon-booking-system' ); ?></a>.</p>
                <table class="form-table" style="max-width:800px;">
                    <tr>
                        <th scope="row"><?php esc_html_e( 'Enable', 'yatoon-booking-system' ); ?></th>
                        <td><label><input type="checkbox" name="yatoon_turnstile_enabled" value="1" <?php checked($turnstile_enabled, true); ?>> <?php esc_html_e( 'Require an invisible Turnstile check before a booking is accepted', 'yatoon-booking-system' ); ?></label></td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e( 'Strict mode', 'yatoon-booking-system' ); ?></th>
                        <td>
                            <label><input type="checkbox" name="yatoon_turnstile_fail_closed" value="1" <?php checked($turnstile_fail_closed, true); ?>> <?php esc_html_e( 'Block bookings when Turnstile cannot return or verify a token', 'yatoon-booking-system' ); ?></label>
                            <p class="description"><?php esc_html_e( '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.', 'yatoon-booking-system' ); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><label for="yatoon_turnstile_site_key"><?php esc_html_e( 'Site Key', 'yatoon-booking-system' ); ?></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"><?php esc_html_e( 'Secret Key', 'yatoon-booking-system' ); ?></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;"><?php esc_html_e( 'Automatic phone number verification', 'yatoon-booking-system' ); ?></h2>
                <p style="color:#555;max-width:700px;">
                    <?php esc_html_e( 'Checks that the phone number is real and dialable the moment a customer submits - no code to type, no extra step.', 'yatoon-booking-system' ); ?>
                    <?php if ($twilio_ready): ?>
                        <?php echo wp_kses_post( __( 'Uses the Twilio credentials already configured in the <strong>SMS</strong> tab.', 'yatoon-booking-system' ) ); ?>
                    <?php else: ?>
                        <?php echo wp_kses_post( __( '<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).', 'yatoon-booking-system' ) ); ?>
                    <?php endif; ?>
                </p>
                <table class="form-table" style="max-width:800px;">
                    <tr>
                        <th scope="row"><?php esc_html_e( 'Enable', 'yatoon-booking-system' ); ?></th>
                        <td><label><input type="checkbox" name="yatoon_phone_lookup_enabled" value="1" <?php checked($phone_lookup_enabled, true);
disabled(!$twilio_ready); ?>> <?php esc_html_e( 'Reject bookings with an invalid or non-existent phone number', 'yatoon-booking-system' ); ?></label></td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e( 'Block VOIP numbers', 'yatoon-booking-system' ); ?></th>
                        <td>
                            <label><input type="checkbox" name="yatoon_phone_lookup_block_voip" value="1" <?php checked($phone_lookup_block_voip, true);
disabled(!$twilio_ready); ?>> <?php esc_html_e( 'Also reject burner/VOIP numbers (e.g. Google Voice, TextNow)', 'yatoon-booking-system' ); ?></label>
                            <p class="description"><?php esc_html_e( '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.', 'yatoon-booking-system' ); ?></p>
                        </td>
                    </tr>
                </table>


                <p style="margin-top:20px;">
                    <button type="submit" class="button button-primary button-large"><?php esc_html_e( 'Save Protection Settings', 'yatoon-booking-system' ); ?></button>
                </p>
            </form>

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

    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.', 'yatoon-booking-system' )
        );
    }

    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.', 'yatoon-booking-system' )
        );
    }

    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', 'yatoon-booking-system' ), __( 'Custom branding, logo styling, and advanced visual controls stay in Pro.', 'yatoon-booking-system' ) );
            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', 'yatoon-booking-system' ), __( 'Google Calendar sync requires an active Pro license.', 'yatoon-booking-system' ) );
            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', 'yatoon-booking-system' ), __( 'Advanced staff controls require an active Pro license.', 'yatoon-booking-system' ) );
            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', 'yatoon-booking-system' ));
        }
        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( sprintf( __( '%s helps growing salons run fewer disconnected tools', 'yatoon-booking-system' ), $feature_name ) ); ?>
                </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 = array(
                                __( '✓ Local WordPress booking', 'yatoon-booking-system' ),
                                __( '✓ Customer booking page', 'yatoon-booking-system' ),
                                __( '✓ Email confirmations', 'yatoon-booking-system' ),
                                __( '✓ Services, staff, and hours', 'yatoon-booking-system' ),
                                __( '✓ Client records and notes', 'yatoon-booking-system' ),
                                __( '✓ Staff portal schedule view', 'yatoon-booking-system' ),
                                __( '— Square sync', 'yatoon-booking-system' ),
                                __( '— Square online payments', 'yatoon-booking-system' ),
                                __( '— SMS reminders', 'yatoon-booking-system' ),
                                __( '— Deposits and no-show automation', 'yatoon-booking-system' ),
                            );
                            foreach ($free_items as $item) {
                                $color = strpos($item, '✓') === 0 ? '#374151' : '#9ca3af';
                                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 = array(
                                __( '✓ Everything in Free', 'yatoon-booking-system' ),
                                __( '✓ Square Appointments sync', 'yatoon-booking-system' ),
                                __( '✓ Square online payments', 'yatoon-booking-system' ),
                                __( '✓ Deposits and prepay', 'yatoon-booking-system' ),
                                __( '✓ SMS via Twilio', 'yatoon-booking-system' ),
                                __( '✓ Google Calendar sync', 'yatoon-booking-system' ),
                                __( '✓ Packages and memberships', 'yatoon-booking-system' ),
                                __( '✓ No-show workflows', 'yatoon-booking-system' ),
                                __( '✓ Advanced brand controls', 'yatoon-booking-system' ),
                                __( '✓ Priority support', 'yatoon-booking-system' ),
                            );
                            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.', 'yatoon-booking-system' )));
        }

        $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', 'yatoon-booking-system' )));
        }
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            wp_send_json_error(array('message' => __( 'Vagaro sync requires an active Pro license.', 'yatoon-booking-system' )));
        }
        $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' => sprintf( __( 'Sync error: %s', 'yatoon-booking-system' ), $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', 'yatoon-booking-system' ) ) );
        }

        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.', 'yatoon-booking-system' ),
                '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.', 'yatoon-booking-system' ) ) );
        }

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

        wp_send_json_success( array(
            'message'  => __( 'Staff Portal page created.', 'yatoon-booking-system' ),
            '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', 'yatoon-booking-system' )));
        }

        // 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_client_secret', 'yatoon_vagaro_region', 'yatoon_vagaro_business_id', 'yatoon_vagaro_timezone',
            // 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',
            'yatoon_portal_signin_label', 'yatoon_portal_signin_bg', 'yatoon_portal_signin_text', 'yatoon_portal_signin_panel_bg',
            'yatoon_portal_email_signin_label', 'yatoon_portal_sms_signin_label',
            // 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',
            // Tax / VAT and invoicing (4.4.3).
            //
            // NOTE FOR FUTURE FIELDS: the General settings screen has TWO save
            // paths, and a new option must be added to BOTH or it silently
            // never persists:
            //   - this method, reached by wp_ajax_yatoon_save_general_settings
            //     (what the "Save Settings" button actually calls; the button
            //     is type="button" and posts over AJAX)
            //   - handle_save_general(), reached by
            //     admin_post_yatoon_save_general_settings (the no-JS fallback)
            // 4.4.3 originally registered the tax fields only in the second
            // one, so the settings screen accepted them, showed no error, and
            // discarded them.
            'yatoon_tax_label', 'yatoon_tax_registration_id',
            'yatoon_invoice_prefix', 'yatoon_currency',
        );
        $protected_empty_keys = array(
            'yatoon_square_access_token',
            'yatoon_vagaro_api_key',
            'yatoon_vagaro_client_secret',
            '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'])))));
        }
        // 4.4.3: tax rate is a percentage, clamped the same way YATOON_Tax does.
        if ( array_key_exists('yatoon_tax_rate', $_POST) ) {
            update_option('yatoon_tax_rate', max(0, min(100, floatval(wp_unslash($_POST['yatoon_tax_rate'])))));
        }

        // 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', 'yatoon_waitlist_claim_minutes',
            // Multi-location (general tab)
            'yatoon_multi_location_enabled',
            // Tax / VAT and invoicing (4.4.3) - see the note in $text_opts.
            'yatoon_tax_enabled', 'yatoon_tax_prices_include', 'yatoon_invoice_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', 'yatoon_ai_reschedule_assistant_enabled',
        );
        foreach ( $checkboxes as $opt ) {
            // Full-form saves explicitly submit unchecked boxes as an empty
            // value. Partial actions (for example, Save Platform) omit every
            // unrelated checkbox and must not silently turn them all off.
            if ( array_key_exists( $opt, $_POST ) ) {
                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!', 'yatoon-booking-system' )));
    }

    public function ajax_update_option() {
        check_ajax_referer('yatoon_option', 'nonce');

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

        $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', 'yatoon-booking-system' ));
        }

        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)) {
            wp_send_json_error( __( 'The database upgrade is incomplete. Open Yatoon Operations and retry migrations.', 'yatoon-booking-system' ) );
        }

        // 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
            $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', 'yatoon-booking-system' ), 'price_modifier' => $price_modifier));
        } else {
            wp_send_json_error(__( 'Failed to update option: ', 'yatoon-booking-system' ) . $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', 'yatoon-booking-system' )); }

        $option_id = isset($_POST['option_id']) ? intval($_POST['option_id']) : 0;
        if (!$option_id) {
            wp_send_json_error(__( 'Invalid option_id', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' )));
        } else {
            wp_send_json_error(__( 'Failed to delete: ', 'yatoon-booking-system' ) . $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', 'yatoon-booking-system' )); }

        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
        if (!$service_id) { wp_send_json_error(__( 'Invalid service_id', 'yatoon-booking-system' )); }

        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', 'yatoon-booking-system' )); }
        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
        // 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.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.sort_order 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"
        );

        // Group each service's own options with it so they're immediately
        // discoverable, instead of every service listed first and every
        // option dumped in one long block at the very end of the picker.
        $options_by_service = array();
        foreach ((array) $rows as $option) {
            $options_by_service[ intval( $option->service_id ) ][] = $option;
        }

        $items = array();
        foreach ((array) $services as $service) {
            $service_id = intval( $service->service_id );
            // The service row is a category container, not a bookable choice.
            // Only its real options/variations belong in this picker.
            $group_label = $service->service_name;
            foreach ( isset( $options_by_service[ $service_id ] ) ? $options_by_service[ $service_id ] : array() as $option ) {
                $items[] = array(
                    'key'              => 'option:' . intval($option->option_id),
                    'source_type'      => 'option',
                    'source_id'        => intval($option->option_id),
                    'service_id'       => $service_id,
                    'group_label'      => $group_label,
                    '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,
                );
            }
        }
        // Any options whose parent service is inactive still get listed
        // (reusable, just grouped separately) rather than silently dropped.
        foreach ( $options_by_service as $service_id => $service_options ) {
            $already_included = false;
            foreach ( (array) $services as $service ) {
                if ( intval( $service->service_id ) === intval( $service_id ) ) { $already_included = true;
break; }
            }
            if ( $already_included ) { continue;
            }
            foreach ( $service_options as $option ) {
                $items[] = array(
                    'key'              => 'option:' . intval($option->option_id),
                    'source_type'      => 'option',
                    'source_id'        => intval($option->option_id),
                    'service_id'       => intval( $service_id ),
                    'group_label'      => $option->service_name . ' (inactive service)',
                    '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', 'yatoon-booking-system' )); }

        $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', 'yatoon-booking-system' ));
        }

        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.', 'yatoon-booking-system' ));
            }
            $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.', 'yatoon-booking-system' ));
            }
            $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')
                );
            }
        }

        // 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
            $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.', 'yatoon-booking-system' ));
                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', 'yatoon-booking-system' ),
                'option_id' => $wpdb->insert_id,
                'square_id' => $square_id,
            ));
        } else {
            wp_send_json_error(__( 'Failed to add option: ', 'yatoon-booking-system' ) . $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', 'yatoon-booking-system' )); }

        $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', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' )));
    }

    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', 'yatoon-booking-system' )));
        }

        $staff_id = isset($_POST['staff_id']) ? intval($_POST['staff_id']) : 0;
        if (!$staff_id) {
            wp_send_json_error(array('message' => __( 'Invalid staff ID', 'yatoon-booking-system' )));
        }

        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.', 'yatoon-booking-system' )));
        }

        $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' => sprintf( __( 'Could not load services: %s', 'yatoon-booking-system' ), $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', 'yatoon-booking-system' )));
        }

        $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', 'yatoon-booking-system' )));
        }

        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', 'yatoon-booking-system' ),
            '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', 'yatoon-booking-system' ));
        }

        $booking_ids = isset($_POST['booking_ids']) ? array_map('intval', $_POST['booking_ids']) : array();

        if (empty($booking_ids)) {
            wp_send_json_error(__( 'No bookings selected', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ));
        }
    }

    /**
     * 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', 'yatoon-booking-system' )));
        }

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

        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', 'yatoon-booking-system' )));
        } else {
            wp_send_json_error(array('message' => __( 'Failed to delete booking', 'yatoon-booking-system' )));
        }
    }

    /**
     * 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', 'yatoon-booking-system' ));
        }

        global $wpdb;
        $table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery
        // 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', 'yatoon-booking-system' ));
        }

        // 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', 'yatoon-booking-system' ));
        }

        $ids = isset($_POST['ids']) ? array_map('intval', $_POST['ids']) : array();

        if (empty($ids)) {
            wp_send_json_error(__( 'No items selected', 'yatoon-booking-system' ));
        }

        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' => sprintf( __( '%d item(s) deleted', 'yatoon-booking-system' ), $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', 'yatoon-booking-system' ));
        }

        global $wpdb;
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery
        // 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', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ) );
        }

        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
	        ) );
	        $advanced = class_exists( 'YATOON_Reporting' ) ? YATOON_Reporting::collect( $date_from, $date_to ) : array();

        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, '.', '' ),
	        ) );
	        if ( $advanced ) {
	            fputcsv( $output, array() );
	            fputcsv( $output, array( 'Business Growth & Forecast' ) );
	            fputcsv( $output, array( 'Unique Clients', 'Repeat Client Rate', 'Retention Rate', 'Rebooking Rate', 'Cancellation Rate', 'No-show Rate', 'Future 30-day Bookings', 'Future 30-day Revenue', 'Estimated Supply Cost', 'Commission', 'Estimated Contribution' ) );
	            fputcsv( $output, array(
	                (int) $advanced['unique_clients'],
	                $advanced['repeat_rate'] . '%', $advanced['retention_rate'] . '%', $advanced['rebook_rate'] . '%',
	                $advanced['cancellation_rate'] . '%', $advanced['no_show_rate'] . '%',
	                (int) $advanced['future_bookings_30'], number_format( (float) $advanced['future_revenue_30'], 2, '.', '' ),
	                number_format( (float) $advanced['supply_cost_total'], 2, '.', '' ), number_format( (float) $advanced['commission_total'], 2, '.', '' ),
	                number_format( (float) $advanced['contribution_after_commission'], 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, '.', '' ) ) ) );
	        }
	        if ( $advanced ) {
	            fputcsv( $output, array() );
	            fputcsv( $output, array( 'Staff Utilization' ) );
	            fputcsv( $output, array( 'Staff', 'Booked Hours', 'Capacity Hours', 'Utilization', 'Revenue' ) );
	            foreach ( (array) $advanced['staff'] as $row ) {
	                fputcsv( $output, $this->csv_row( array( $row->name, round( $row->booked_minutes / 60, 2 ), round( $row->capacity_minutes / 60, 2 ), $row->utilization . '%', number_format( (float) $row->revenue, 2, '.', '' ) ) ) );
	            }
	            fputcsv( $output, array() );
	            fputcsv( $output, array( 'Booking Sources' ) );
	            fputcsv( $output, array( 'Source', 'Bookings', 'Revenue' ) );
	            foreach ( (array) $advanced['sources'] as $row ) {
	                fputcsv( $output, $this->csv_row( array( $row->source, (int) $row->bookings, number_format( (float) $row->revenue, 2, '.', '' ) ) ) );
	            }
	            fputcsv( $output, array() );
	            fputcsv( $output, array( 'Service Revenue & Estimated Profit' ) );
	            fputcsv( $output, array( 'Service', 'Bookings', 'Revenue', 'Estimated Supply Cost', 'Before Commission' ) );
	            foreach ( (array) $advanced['services'] as $row ) {
	                fputcsv( $output, $this->csv_row( array(
	                    $row->name,
	                    (int) $row->bookings,
	                    number_format( (float) $row->revenue, 2, '.', '' ),
	                    number_format( (float) $row->supply_cost, 2, '.', '' ),
	                    number_format( (float) $row->profit_before_commission, 2, '.', '' ),
	                ) ) );
	            }
	        }

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

    /** Export the same report snapshot as a portable PDF. */
    public function ajax_export_reports_pdf() {
        check_ajax_referer( 'yatoon_export_reports', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) || ! class_exists( 'YATOON_Reporting' ) ) {
            wp_die( esc_html__( 'Permission denied.', 'yatoon-booking-system' ) );
        }
        list( $from, $to ) = YATOON_Reporting::normalize_dates(
            sanitize_text_field( wp_unslash( $_GET['date_from'] ?? '' ) ),
            sanitize_text_field( wp_unslash( $_GET['date_to'] ?? '' ) )
        );
        YATOON_Reporting::pdf_response( YATOON_Reporting::collect( $from, $to ) );
    }

    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', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }

        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.', 'yatoon-booking-system' )));
        }

        $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: " . yatoon_format_local('F j, Y', $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-booking-system' )));
        }

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

    /**
     * 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-booking-system' ) ) ); }

        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.', 'yatoon-booking-system' ) ) );
    }

    /**
     * AJAX: Bulk-delete one or more client profiles. This only removes the
     * client's profile row (name/notes/tags/preferences) - their booking
     * history stays intact in the bookings table, just no longer linked to
     * a client profile.
     */
    public function ajax_delete_clients() {
        check_ajax_referer( 'yatoon_client_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'yatoon-booking-system' ) ) );
        }

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

        $ids = isset( $_POST['client_ids'] ) ? (array) wp_unslash( $_POST['client_ids'] ) : array();
        $ids = array_filter( array_map( 'intval', $ids ) );
        if ( empty( $ids ) ) {
            wp_send_json_error( array( 'message' => __( 'No clients selected.', 'yatoon-booking-system' ) ) );
        }

        $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
        $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE id IN ({$placeholders})", $ids ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        wp_send_json_success( array(
                    'message' => sprintf( _n( '%d client deleted.', '%d clients deleted.', count( $ids ), 'yatoon-booking-system' ), count( $ids ) ),
            'deleted' => count( $ids ),
        ) );
    }

    /**
     * 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', 'yatoon-booking-system' ) ) ); }

        $bookings = yatoon_db_get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            "SELECT b.id, 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
        ) );

        $looks = class_exists( 'YATOON_Client_Looks' ) ? YATOON_Client_Looks::get_looks_for_client( $client->id ) : array();

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

    /**
     * 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', 'yatoon-booking-system' ));
        }

        $ids = isset($_POST['ids']) ? array_map('intval', $_POST['ids']) : array();

        if (empty($ids)) {
            wp_send_json_error(__( 'No messages selected', 'yatoon-booking-system' ));
        }

        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' => sprintf( __( '%d message(s) deleted', 'yatoon-booking-system' ), $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', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ));
        }
        if ($status === 'no_show') {
            do_action('yatoon_booking_marked_no_show', $id);
        }
        if ($status === 'cancelled') {
            $cancelled_booking = yatoon_db_get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}yatoon_bookings WHERE id=%d", $id));
            if ($cancelled_booking) { do_action('yatoon_booking_cancelled', $cancelled_booking);
            }
        }
        do_action( 'yatoon_booking_status_changed', $id, $status );
        wp_send_json_success(array('message' => __( 'Booking status updated.', 'yatoon-booking-system' )));
    }

    /**
     * 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', 'yatoon-booking-system' ));
        }

        $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', 'partially_refunded', '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', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ));
        }
        $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.', 'yatoon-booking-system' )));
    }

    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', 'yatoon-booking-system' )));
        }

        $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.', 'yatoon-booking-system' )));
        }

        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.', 'yatoon-booking-system' )));
        }

        $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', 'yatoon-booking-system' )));
        }

        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.', 'yatoon-booking-system' )));
        }

        $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', 'whatsapp', 'email_whatsapp', 'all'), 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.', 'yatoon-booking-system' )));
        }

        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.', 'yatoon-booking-system' )));
        }

        try {
            $tz       = yatoon_timezone();
            $start_dt = new DateTime($start_raw, $tz);
        } catch (Exception $e) {
            wp_send_json_error(array('message' => __( 'Invalid date or time.', 'yatoon-booking-system' )));
        }

        $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.', 'yatoon-booking-system' )));
        }

        $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.', 'yatoon-booking-system' )));
        }

        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', 'email_whatsapp', 'all'), true) && class_exists('YATOON_Notifications')) {
                    $notif = new YATOON_Notifications();
                    $notif->send_reschedule_email($fresh_arr);
                }
                if (in_array($notify_method, array('sms', 'both', 'all'), true) && class_exists('YATOON_Twilio')) {
                    YATOON_Twilio::send_reschedule_sms($fresh_arr);
                }
                if (class_exists('YATOON_WhatsApp') && in_array($notify_method, array('whatsapp', 'email_whatsapp', 'all'), true)) {
                    YATOON_WhatsApp::send_reschedule($fresh_arr);
                }
            }
        }

        $integration_booking = yatoon_db_get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}yatoon_bookings WHERE id=%d", $booking_id));
        if ($integration_booking) { do_action('yatoon_booking_rescheduled', $integration_booking);
        }

        wp_send_json_success(array(
            'message'    => __( 'Booking rescheduled.', 'yatoon-booking-system' ),
            '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', 'yatoon-booking-system' ));
        }
        $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', 'yatoon-booking-system' ));
        }
        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', 'yatoon-booking-system' ));
        }
        $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', 'yatoon-booking-system' ));
        }
        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', 'yatoon-booking-system' )));
        }

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

        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.', 'yatoon-booking-system' )));
        }

        $business = get_option('yatoon_business_name', get_bloginfo('name'));
        $pay_url = get_option('yatoon_payment_request_url', '');
        if (!$pay_url) {
            wp_send_json_error(array('message' => __( 'Configure a Payment Request URL in Settings before sending a payment request.', 'yatoon-booking-system' )));
        }

        /* 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>' . sprintf( esc_html__( 'Hi %s,', 'yatoon-booking-system' ), esc_html($booking->customer_name) ) . '</p>'
              . '<p>' . esc_html__( 'This is a friendly payment reminder for your appointment.', 'yatoon-booking-system' ) . '</p>'
              . '<p><strong>' . esc_html__( 'Service:', 'yatoon-booking-system' ) . '</strong> ' . esc_html($booking->service_name ?: __( 'Appointment', 'yatoon-booking-system' )) . '<br>'
              . '<strong>' . esc_html__( 'Date:', 'yatoon-booking-system' ) . '</strong> ' . esc_html(gmdate('F j, Y g:i A', strtotime($booking->start_time))) . '<br>'
              . '<strong>' . esc_html__( 'Total:', 'yatoon-booking-system' ) . '</strong> $' . esc_html(number_format((float) $booking->total_price, 2)) . '</p>';
        $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;">' . esc_html__( 'Pay now', 'yatoon-booking-system' ) . '</a></p>';
        $body .= '<p>' . esc_html__( 'If you already paid, please ignore this message. Thank you!', 'yatoon-booking-system' ) . '</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-booking-system' )));
        }
        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.', 'yatoon-booking-system' )));
    }

    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', 'yatoon-booking-system' )));
        }
        $name  = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : __( 'there', 'yatoon-booking-system' );
        $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
        if (!$email || !is_email($email)) {
            wp_send_json_error(array('message' => __( 'Customer email missing.', 'yatoon-booking-system' )));
        }
        $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>' . sprintf( esc_html__( 'Hi %s,', 'yatoon-booking-system' ), esc_html($name ?: __( 'there', 'yatoon-booking-system' )) ) . '</p>'
              . '<p>' . esc_html__( 'It has been a little while since your last visit. We would love to welcome you back.', 'yatoon-booking-system' ) . '</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;">' . esc_html__( 'Book again', 'yatoon-booking-system' ) . '</a></p>';
        }
        $body .= '<p>' . esc_html__( 'Thank you for choosing us.', 'yatoon-booking-system' ) . '</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.', 'yatoon-booking-system' )));
        }
        wp_send_json_success(array('message' => __( 'Recall email sent.', 'yatoon-booking-system' )));
    }

    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', 'yatoon-booking-system' )));
        }
        if (!function_exists('ybs_is_pro') || !ybs_is_pro()) {
            wp_send_json_error(array('message' => __( 'SMS testing requires an active Pro license.', 'yatoon-booking-system' )));
        }
        if (!class_exists('YATOON_Twilio')) {
            wp_send_json_error(array('message' => __( 'Twilio class is not available.', 'yatoon-booking-system' )));
        }
        $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.'));
    }

    public function ajax_whatsapp_test() {
        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() || !class_exists('YATOON_WhatsApp')) {
            wp_send_json_error(array('message' => __( 'WhatsApp Cloud API requires an active Pro license.', 'yatoon-booking-system' )));
        }
        $phone = isset($_POST['test_phone']) ? sanitize_text_field(wp_unslash($_POST['test_phone'])) : '';
        $result = YATOON_WhatsApp::test_send($phone);
        if (!empty($result['success'])) {
            wp_send_json_success(array('message' => $result['message']));
        }
        wp_send_json_error(array('message' => $result['message'] ?? __( 'WhatsApp test failed.', 'yatoon-booking-system' )));
    }

    /**
     * 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', 'yatoon-booking-system' ));
        }

        global $wpdb;
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery
        // 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', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ));
        }

        $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
        $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', 'yatoon-booking-system' ));
        }
    }

    /**
     * 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', 'yatoon-booking-system' ));
        }

        $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
	        $tax_rate = isset( $_POST['tax_rate'] ) && '' !== trim( (string) $_POST['tax_rate'] )
	            ? max( 0, min( 100, (float) $_POST['tax_rate'] ) )
	            : null;
	        $estimated_cost = isset( $_POST['estimated_cost'] ) ? max( 0, (float) $_POST['estimated_cost'] ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $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', 'yatoon-booking-system' ));
        }

        $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) {
            wp_send_json_error( __( 'The database upgrade is incomplete. Open Yatoon Operations and retry migrations.', 'yatoon-booking-system' ) );
        }

        $data = array(
            'name' => $name,
            'description' => $description,
            'duration_minutes' => $duration,
			'price' => $price,
			'tax_rate' => $tax_rate,
			'estimated_cost' => $estimated_cost,
            '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', '%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
            // 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', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ));
    }

    /**
     * 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', 'yatoon-booking-system' ));
        }

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

        if ($category === '') {
            wp_send_json_error(__( 'Category is required', 'yatoon-booking-system' ));
        }

        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', 'yatoon-booking-system' ));
    }

    public function ajax_save_service_images() {
        check_ajax_referer('yatoon_service', 'nonce');
        if (!current_user_can('manage_options')) { wp_send_json_error(__( 'Permission denied', 'yatoon-booking-system' ));
        }

        $service_id = absint($_POST['service_id'] ?? 0);
        if (!$service_id) { wp_send_json_error(__( 'Missing service ID', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }

        $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', 'yatoon-booking-system' ));
        }
    }

    /**
     * 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';
    }
}
