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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

if ( ! function_exists( 'yatoon_sanitize_setting' ) ) {
    function yatoon_sanitize_setting($value) {
        if (is_array($value)) {
            return array_map('yatoon_sanitize_setting', $value);
        }

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

        return sanitize_textarea_field(wp_unslash($value));
    }
}

class YATOON_Ajax {
    
    private $square_api;
    
    public function __construct() {
        // -- Per-location Square API init ---------------------------------------
        // When multi-location mode is on, resolve the Square Location ID from
        // the location passed in the request (availability checks, bookings).
        // Falls back to the global setting automatically via the constructor.
        $sq_location_id = '';
        if ( get_option( 'yatoon_multi_location_enabled', '0' ) === '1' ) {
            $raw_loc = isset( $_REQUEST['yatoon_location_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['yatoon_location_id'] ) ) : '';
            if ( $raw_loc !== '' ) {
                $sq_location_id = YATOON_Square_API::get_square_location_id_for( $raw_loc );
            }
        }
        $this->square_api = new YATOON_Square_API( $sq_location_id );
        
        // Frontend AJAX
        add_action('wp_ajax_yatoon_get_service_options', array($this, 'get_service_options'));
        add_action('wp_ajax_nopriv_yatoon_get_service_options', array($this, 'get_service_options'));
        
        add_action('wp_ajax_yatoon_get_available_staff', array($this, 'get_available_staff'));
        add_action('wp_ajax_nopriv_yatoon_get_available_staff', array($this, 'get_available_staff'));
        
        // Admin diagnostic
        add_action('wp_ajax_yatoon_diagnose_service', array($this, 'diagnose_service'));
        add_action('wp_ajax_yatoon_fix_variation_ids', array($this, 'fix_variation_ids'));
        
        add_action('wp_ajax_yatoon_get_available_times', array($this, 'get_available_times'));
        add_action('wp_ajax_nopriv_yatoon_get_available_times', array($this, 'get_available_times'));

        add_action('wp_ajax_yatoon_get_next_available', array($this, 'get_next_available'));
        add_action('wp_ajax_nopriv_yatoon_get_next_available', array($this, 'get_next_available'));
        
        add_action('wp_ajax_yatoon_create_booking', array($this, 'create_booking'));
        add_action('wp_ajax_nopriv_yatoon_create_booking', array($this, 'create_booking'));
        add_action('wp_ajax_yatoon_upload_design_reference', array($this, 'upload_design_reference'));
        add_action('wp_ajax_nopriv_yatoon_upload_design_reference', array($this, 'upload_design_reference'));
        add_action('yatoon_booking_created', array($this, 'finalize_design_reference'), 90, 1);
        add_action('yatoon_cleanup_design_uploads', array($this, 'cleanup_design_uploads'));
        if (!wp_next_scheduled('yatoon_cleanup_design_uploads')) {
            wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'yatoon_cleanup_design_uploads');
        }

        // Admin: toggle staff online booking status
        add_action('wp_ajax_yatoon_toggle_staff_bookable', array($this, 'toggle_staff_bookable'));
        
        // Admin: save service order
        add_action('wp_ajax_yatoon_update_service_order', array($this, 'update_service_order'));
        add_action('wp_ajax_yatoon_rebuild_staff_services', array($this, 'rebuild_staff_services'));
        
        // Admin: reorder services within a multi-service booking group
        add_action('wp_ajax_yatoon_reorder_booking_group', array($this, 'reorder_booking_group'));
        
        // Frontend: send contact message
        add_action('wp_ajax_yatoon_send_contact_message', array($this, 'send_contact_message'));
        add_action('wp_ajax_nopriv_yatoon_send_contact_message', array($this, 'send_contact_message'));

        // Frontend: join waitlist
        add_action('wp_ajax_yatoon_join_waitlist', array($this, 'join_waitlist'));
        add_action('wp_ajax_nopriv_yatoon_join_waitlist', array($this, 'join_waitlist'));
        // Admin: debug bookings for a specific date
        add_action('wp_ajax_yatoon_debug_date_bookings', array($this, 'debug_date_bookings'));
        // Admin: query Square API directly for a date+service
        add_action('wp_ajax_yatoon_debug_square_availability', array($this, 'debug_square_availability'));
        // Admin: cancel a booking (update local DB + cancel in Square)
        add_action('wp_ajax_yatoon_cancel_booking', array($this, 'cancel_booking_admin'));
        // Admin: permanently delete a single booking from local DB
        add_action('wp_ajax_yatoon_delete_single_booking', array($this, 'delete_single_booking'));
        // Admin: sync bookings from Square (pull status updates)
        add_action('wp_ajax_yatoon_sync_bookings_from_square', array($this, 'sync_bookings_from_square'));
    }

    /**
     * Send contact message (Text Us form)
     */
    public function send_contact_message() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');

        $this->enforce_public_rate_limit('contact', 5, 10);
        
        $name    = isset($_POST['name']) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
        $phone   = isset($_POST['phone']) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';
        $message = isset($_POST['message']) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
        
        if (empty($name) || empty($phone) || empty($message)) {
            wp_send_json_error(array('message' => 'All fields required'));
        }

        $client_ip = $this->get_client_ip();
        
        // Save to database
        global $wpdb;
        $table = $wpdb->prefix . 'yatoon_contact_messages';
        yatoon_db_insert($table, array(
            'name'       => $name,
            'phone'      => $phone,
            'message'    => $message,
            'ip_address' => $client_ip,
            'created_at' => current_time('mysql')
        ));
        
        // Send email notification
        $to      = get_option('yatoon_contact_email', '');
        $business_name = get_option('yatoon_business_name', 'Your Business');
        $subject = '[' . $business_name . '] New contact message from ' . $name;
        $body    = "Name: {$name}\n";
        $body   .= "Phone: {$phone}\n";
        $body   .= "Message:\n{$message}\n\n";
        $body   .= "---\n";
        $body   .= "Sent from: " . home_url() . "\n";
        $body   .= "IP: " . $client_ip;
        
        if (is_email($to)) {
            wp_mail($to, $subject, $body);
        }
        
        wp_send_json_success(array('message' => 'Message sent'));
    }

    /**
     * Admin: save service order
     */
    public function update_service_order() {
        check_ajax_referer('yatoon_service_order', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Unauthorized'));
        }
        
        global $wpdb;
        $table = $wpdb->prefix . 'yatoon_services';
        $order = isset($_POST['order']) ? $_POST['order'] : array();
        
        foreach ($order as $item) {
            yatoon_db_update(
                $table,
                array('sort_order' => intval($item['sort_order'])),
                array('id' => intval($item['id']))
            );
        }
        
        wp_send_json_success(array('message' => 'Order saved'));
    }

    /**
     * Admin: Swap the order of two services within a multi-service booking group.
     *
     * When swapped, both bookings in Square are cancelled and re-created with
     * the corrected sequential times. Locally, start_time / end_time and
     * service_order are updated to match.
     *
     * POST params:
     *   booking_id_a  - local ID of booking A (the one being moved)
     *   booking_id_b  - local ID of booking B (the one it swaps with)
     *   nonce         - yatoon_admin_nonce
     */
    public function reorder_booking_group() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Unauthorized'));
        }

        global $wpdb;
        $table_bookings  = $wpdb->prefix . 'yatoon_bookings';
        $table_services  = $wpdb->prefix . 'yatoon_services';
        $table_options   = $wpdb->prefix . 'yatoon_service_options';

        $id_a = intval($_POST['booking_id_a'] ?? 0);
        $id_b = intval($_POST['booking_id_b'] ?? 0);
        if (!$id_a || !$id_b || $id_a === $id_b) {
            wp_send_json_error(array('message' => 'Invalid booking IDs'));
        }

        $bk_a = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_bookings WHERE id=%d", $id_a));
        $bk_b = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_bookings WHERE id=%d", $id_b));

        if (!$bk_a || !$bk_b) {
            wp_send_json_error(array('message' => 'Booking(s) not found'));
        }
        if ($bk_a->group_booking_id !== $bk_b->group_booking_id || empty($bk_a->group_booking_id)) {
            wp_send_json_error(array('message' => 'Bookings are not in the same group'));
        }

        // Load ALL bookings in this group, ordered by service_order
        $group_id   = $bk_a->group_booking_id;
        $group_rows = yatoon_db_get_results($wpdb->prepare(
            "SELECT * FROM $table_bookings WHERE group_booking_id=%s ORDER BY service_order ASC, start_time ASC",
            $group_id
        ));
        if (count($group_rows) < 2) {
            wp_send_json_error(array('message' => 'Group must have at least 2 bookings'));
        }

        // Swap service_order values between A and B
        $order_a = intval($bk_a->service_order);
        $order_b = intval($bk_b->service_order);
        yatoon_db_update($table_bookings, array('service_order' => $order_b), array('id' => $id_a));
        yatoon_db_update($table_bookings, array('service_order' => $order_a), array('id' => $id_b));

        // Re-load group in new order
        $group_rows = yatoon_db_get_results($wpdb->prepare(
            "SELECT * FROM $table_bookings WHERE group_booking_id=%s ORDER BY service_order ASC",
            $group_id
        ));

        // Determine the earliest start time for the whole group (the anchor)
        $all_starts = array_map(fn($r) => strtotime($r->start_time), $group_rows);
        $anchor_ts  = min($all_starts);
        $anchor_dt  = new DateTime('@' . $anchor_ts);
        $timezone   = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz_obj     = new DateTimeZone($timezone);
        $anchor_dt->setTimezone($tz_obj);

        $platform = sanitize_key((string) get_option('yatoon_active_platform', 'local'));
        if (!in_array($platform, array('local', 'square', 'vagaro'), true)) {
            $platform = 'local';
        }

        // Recompute and update times for each booking in the new order
        $cumulative = 0;
        $updated    = array();
        foreach ($group_rows as $bk) {
            $svc = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_services WHERE id=%d", $bk->service_id));
            if (!$svc) continue;

            $dur = intval($svc->duration_minutes);
            // Check if this booking has an option that overrides duration
            $opt = yatoon_db_get_row($wpdb->prepare(
                "SELECT o.duration_minutes FROM {$wpdb->prefix}yatoon_booking_options bo
                 INNER JOIN $table_options o ON bo.option_id = o.id
                 WHERE bo.booking_id = %d AND o.duration_minutes > 0 LIMIT 1",
                $bk->id
            ));
            if ($opt) $dur = intval($opt->duration_minutes);

            $new_start = clone $anchor_dt;
            if ($cumulative > 0) $new_start->modify("+{$cumulative} minutes");
            $new_end = clone $new_start;
            $new_end->modify("+{$dur} minutes");

            $start_str = $new_start->format('Y-m-d H:i:s');
            $end_str   = $new_end->format('Y-m-d H:i:s');

            // Update local DB
            yatoon_db_update($table_bookings,
                array('start_time' => $start_str, 'end_time' => $end_str),
                array('id' => $bk->id)
            );

            // Update Square booking if synced
            if ($platform === 'square' && !empty($bk->square_booking_id)) {
                // Cancel old Square booking
                $this->square_api->cancel_booking($bk->square_booking_id);

                // Get variation ID for this service
                $var_id  = null;
                $var_ver = null;
                $opt_row = yatoon_db_get_row($wpdb->prepare(
                    "SELECT o.square_id, o.square_version FROM {$wpdb->prefix}yatoon_booking_options bo
                     INNER JOIN $table_options o ON bo.option_id = o.id
                     WHERE bo.booking_id = %d AND o.square_id != '' LIMIT 1",
                    $bk->id
                ));
                if ($opt_row) { $var_id = $opt_row->square_id; $var_ver = $opt_row->square_version; }
                if (!$var_id && !empty($svc->square_id)) { $var_id = $svc->square_id; $var_ver = $svc->square_version ?? null; }

                if ($var_id) {
                    $table_stf = $wpdb->prefix . 'yatoon_staff';
                    $stf = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_stf WHERE id=%d", $bk->staff_id));
                    $seg = array(
                        'duration_minutes'     => $dur,
                        'service_variation_id' => $var_id,
                        'team_member_id'       => $stf ? $stf->square_id : '',
                    );
                    if ($var_ver) $seg['service_variation_version'] = intval($var_ver);

                    $new_sq_resp = $this->square_api->create_booking(array(
                        'location_id'          => get_option('yatoon_square_location_id'),
                        'customer_id'          => '', // will be empty - acceptable for reschedule
                        'start_at'             => $new_start->format('c'),
                        'appointment_segments' => array($seg),
                    ));

                    if (!is_wp_error($new_sq_resp) && isset($new_sq_resp['booking']['id'])) {
                        yatoon_db_update($table_bookings,
                            array('square_booking_id' => $new_sq_resp['booking']['id'], 'square_synced' => 1),
                            array('id' => $bk->id)
                        );
                    }
                }
            }

            $updated[] = array(
                'id'         => $bk->id,
                'start_time' => $start_str,
                'end_time'   => $end_str,
                'service_order' => $cumulative === 0 ? 0 : $cumulative,
            );

            $cumulative += $dur;
        }

        wp_send_json_success(array(
            'message' => 'Booking order updated successfully.',
            'group'   => $updated,
        ));
    }

    /**
     * Admin: Rebuild staff-service associations from Square booking profiles.
     * This re-runs the same logic as the cron sync but on-demand.
     * Fixes cases where yatoon_staff_services table is empty due to the variation-ID bug.
     */
    public function rebuild_staff_services() {
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Unauthorized'));
        }

        global $wpdb;
        $table_staff          = $wpdb->prefix . 'yatoon_staff';
        $table_staff_services = $wpdb->prefix . 'yatoon_staff_services';
        $table_options        = $wpdb->prefix . 'yatoon_service_options';
        $table_services       = $wpdb->prefix . 'yatoon_services';

        // Fetch booking profiles from Square
        $profiles_resp = $this->square_api->get_team_member_booking_profiles();
        if (is_wp_error($profiles_resp)) {
            wp_send_json_error(array('message' => 'Failed to fetch Square booking profiles: ' . $profiles_resp->get_error_message()));
        }

        $staff_service_map = array(); // square_staff_id => [square_variation_ids]
        if (isset($profiles_resp['team_member_booking_profiles'])) {
            foreach ($profiles_resp['team_member_booking_profiles'] as $profile) {
                if (!empty($profile['is_bookable']) && !empty($profile['team_member_id']) && !empty($profile['service_variation_ids'])) {
                    $staff_service_map[$profile['team_member_id']] = $profile['service_variation_ids'];
                }
            }
        }

        $updated = 0;
        $skipped = 0;
        $fallback_used = false;

        // Get all local staff
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $all_staff = yatoon_db_get_results("SELECT id, name, square_id FROM $table_staff WHERE is_bookable = 1");

        // Get all active local service IDs (for fallback)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $all_service_ids = yatoon_db_get_col("SELECT id FROM $table_services WHERE is_active = 1");
        $all_service_ids = array_map('intval', $all_service_ids);

        foreach ($all_staff as $staff_row) {
            $square_id = $staff_row->square_id;
            $new_service_ids = array();

            if (!empty($square_id) && isset($staff_service_map[$square_id]) && !empty($staff_service_map[$square_id])) {
                // Square has service data for this staff - use it
                foreach ($staff_service_map[$square_id] as $square_variation_id) {
                    $opt_row = yatoon_db_get_row($wpdb->prepare(
                        "SELECT service_id FROM $table_options WHERE square_id = %s LIMIT 1",
                        $square_variation_id
                    ));
                    if ($opt_row && $opt_row->service_id) {
                        $new_service_ids[] = intval($opt_row->service_id);
                    } else {
                        $svc_row = yatoon_db_get_row($wpdb->prepare(
                            "SELECT id FROM $table_services WHERE square_id = %s LIMIT 1",
                            $square_variation_id
                        ));
                        if ($svc_row) $new_service_ids[] = intval($svc_row->id);
                    }
                }
                $new_service_ids = array_unique($new_service_ids);
            } else {
                // Square has no service_variation_ids for this staff (common when not configured in Square Appointments)
                // Fallback: assign ALL active services so availability can be checked
                $new_service_ids = $all_service_ids;
                $fallback_used = true;
            }

            if (!empty($new_service_ids)) {
                yatoon_db_delete($table_staff_services, array('staff_id' => $staff_row->id));
                foreach ($new_service_ids as $svc_id) {
                    yatoon_db_insert($table_staff_services, array(
                        'staff_id'   => $staff_row->id,
                        'service_id' => $svc_id,
                    ), array('%d', '%d'));
                }
                $updated++;
            } else {
                $skipped++;
            }
        }

        $note = $fallback_used ? ' (Square has no service assignments configured - assigned all services as fallback)' : '';
        wp_send_json_success(array(
            'message' => "OK Done! Updated $updated staff member(s) with service assignments.$note $skipped skipped.",
            'updated' => $updated,
            'skipped' => $skipped,
            'fallback' => $fallback_used,
        ));
    }

    /**
     * Admin: toggle staff is_bookable status
     */
    public function toggle_staff_bookable() {
        check_ajax_referer('yatoon_toggle_staff', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Unauthorized'));
        }
        global $wpdb;
        $staff_id = intval($_POST['staff_id']);
        $bookable  = intval($_POST['bookable']) ? 1 : 0;
        $result = yatoon_db_update(
            $wpdb->prefix . 'yatoon_staff',
            array('is_bookable' => $bookable),
            array('id' => $staff_id)
        );
        if ($result !== false) {
            wp_send_json_success(array('bookable' => $bookable));
        } else {
            wp_send_json_error(array('message' => 'DB error'));
        }
    }
    
    /**
     * Get service options
     */
    public function get_service_options() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');
        
        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
        
        if (!$service_id) {
            wp_send_json_error(array('message' => __('Invalid service ID', 'yatoon-booking-system')));
        }
        
        global $wpdb;
        $table_name = $wpdb->prefix . 'yatoon_service_options';
        $table_services = $wpdb->prefix . 'yatoon_services';
        
        // Get base service price
        $base_service = yatoon_db_get_row($wpdb->prepare("
            SELECT price FROM $table_services WHERE id = %d
        ", $service_id));
        
        $base_price = $base_service ? floatval($base_service->price) : 0;
        
        // Get all options
        $options = yatoon_db_get_results($wpdb->prepare("
            SELECT * FROM $table_name 
            WHERE service_id = %d 
            ORDER BY sort_order ASC, name ASC
        ", $service_id));
        
        // Calculate absolute price for each option
        // Also find linked_service_id: if this option's square_id matches a service's square_id,
        // record that service's DB id so the frontend can detect duplicate bookings by ID.
        foreach ($options as $option) {
            $option->absolute_price = $base_price + floatval($option->price_modifier);
            $option->linked_service_id = null;
            $option->allow_design_upload = 0;
            if (!empty($option->square_id)) {
                $linked = yatoon_db_get_row($wpdb->prepare(
                    "SELECT linked.id, linked.allow_design_upload
                     FROM $table_services linked
                     LEFT JOIN $table_name source_option ON source_option.service_id = linked.id
                     WHERE linked.square_id = %s OR source_option.square_id = %s
                     ORDER BY linked.allow_design_upload DESC
                     LIMIT 1",
                    $option->square_id,
                    $option->square_id
                ));
                if ($linked) {
                    $option->linked_service_id = intval($linked->id);
                    $option->allow_design_upload = !empty($linked->allow_design_upload) ? 1 : 0;
                }
            }
        }
        
        wp_send_json_success(array('options' => $options, 'base_price' => $base_price));
    }
    
    /**
     * Get available staff
     */
    public function get_available_staff() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');
        
        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
        
        if (!$service_id) {
            wp_send_json_error(array('message' => __('Invalid service ID', 'yatoon-booking-system')));
        }
        
        global $wpdb;
        $table_staff = $wpdb->prefix . 'yatoon_staff';
        $table_staff_services = $wpdb->prefix . 'yatoon_staff_services';
        $table_services = $wpdb->prefix . 'yatoon_services';
        
        // Get service name for logging
        $service = yatoon_db_get_row($wpdb->prepare(
            "SELECT name FROM $table_services WHERE id = %d", $service_id
        ));
        $service_name = $service ? $service->name : "Unknown ($service_id)";
        
        // Check if this service has specific staff assigned
        $service_staff = yatoon_db_get_results($wpdb->prepare("
            SELECT DISTINCT s.*
            FROM $table_staff s
            INNER JOIN $table_staff_services ss ON s.id = ss.staff_id
            WHERE s.is_active = 1
            AND s.is_bookable = 1
            AND ss.service_id = %d
            ORDER BY s.sort_order ASC, s.name ASC
        ", $service_id));

        if (!empty($service_staff)) {
            // Service has assigned staff - show only them
            $staff = $service_staff;
        } else {
            // Service has no staff assignment - show all bookable staff (fallback)
            // 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_staff
                WHERE is_active = 1 AND is_bookable = 1
                ORDER BY sort_order ASC, name ASC
            ");
        }
        
        // Only add "Any staff" option when there are 2+ staff members
        if (count($staff) >= 2) {
            $any_staff = (object)array(
                'id' => 0,
                'name' => 'Any staff',
                'is_any_staff' => true,
                'is_active' => 1,
                'is_bookable' => 1
            );
            array_unshift($staff, $any_staff);
        }

        // Enrich staff with avg rating and booking count from reviews
        $reviews_table  = $wpdb->prefix . 'yatoon_reviews';
        $bookings_table = $wpdb->prefix . 'yatoon_bookings';
        foreach ($staff as $member) {
            if (empty($member->id) || intval($member->id) === 0) continue;
            $member->avg_rating = floatval(yatoon_db_get_var($wpdb->prepare(
                "SELECT AVG(r.rating) FROM $reviews_table r
                 INNER JOIN $bookings_table b ON b.id = r.booking_id
                 WHERE b.staff_id = %d AND r.status = 'approved'",
                intval($member->id)
            )));
            $member->review_count = intval(yatoon_db_get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM $reviews_table r
                 INNER JOIN $bookings_table b ON b.id = r.booking_id
                 WHERE b.staff_id = %d AND r.status = 'approved'",
                intval($member->id)
            )));
            $member->booking_count = intval(yatoon_db_get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM $bookings_table WHERE staff_id = %d AND status NOT IN ('cancelled','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')",
                intval($member->id)
            )));
        }

        wp_send_json_success(array('staff' => $staff));
    }
    
    /**
     * Get available time slots
     */
    public function get_available_times() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');

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

        // -- Global closed dates check ------------------------------------------
        if ($date) {
            $closed_raw   = get_option('yatoon_closed_dates', '');
            $closed_label = get_option('yatoon_closed_label', 'We are closed on this date.');
            $closed_list  = array_filter(array_map('trim', explode("\n", $closed_raw)));
            if (in_array($date, $closed_list, true)) {
                wp_send_json_success(array(
                    'times'   => array(),
                    'message' => $closed_label,
                    'closed'  => true,
                ));
                return;
            }
        }
        // -- End closed dates ---------------------------------------------------
        $timezone      = ( wp_timezone_string() ?: 'America/Chicago' );
        // Extra minutes from add-on options (e.g. Remove Acrylic = 20 min).
        // Square's availability API only accounts for the main variation duration,
        // so we must filter slots locally to ensure the full window (main + addons) is available.
        $addon_duration = isset($_POST['addon_duration']) ? max(0, intval($_POST['addon_duration'])) : 0;

        // Add-on option IDs - used to do a real-time multi-segment Square availability query
        // so Square itself validates the combined duration fits in the available window.
        $addon_option_ids = array();
        if (!empty($_POST['addon_option_ids'])) {
            $raw_addon = json_decode(stripslashes($_POST['addon_option_ids']), true);
            if (is_array($raw_addon)) {
                $addon_option_ids = array_map('intval', $raw_addon);
            }
        }

        // Total appointment duration hint (sum of all services' durations) sent from frontend.
        // Used by the latest-end-time cutoff filter below. 0 = unknown, fallback to per-service lookup.
        $total_duration_hint = isset($_POST['total_duration']) ? max(0, intval($_POST['total_duration'])) : 0;

        // -- Latest-end-time cutoff ---------------------------------------------
        // No appointment should end later than this time (configurable, default 8:00 PM).
        // Applied to every time-slot array before it is returned to the frontend.
        $latest_end_hour   = intval(get_option('yatoon_latest_end_hour', 20));   // 20 = 8 PM
        $latest_end_minute = intval(get_option('yatoon_latest_end_minute', 0));

        /**
         * Filter a times array, removing any slot whose end time (start + duration) is
         * after the configured latest-end cutoff.
         *
         * @param array $times          Array of ['time'=>'H:i', 'display'=>..., 'start_at'=>...].
         * @param int   $duration_min   Total appointment duration in minutes.
         * @param string $date          'Y-m-d' string for the day.
         * @return array Filtered times.
         */
        $apply_cutoff = function(array $times, int $duration_min, string $slot_date) use ($latest_end_hour, $latest_end_minute, $timezone) {
            if ($duration_min <= 0 || empty($times)) return $times;
            $tz_obj = new DateTimeZone($timezone ?: 'America/New_York');
            $cutoff = new DateTime($slot_date . ' ' . str_pad($latest_end_hour, 2, '0', STR_PAD_LEFT) . ':' . str_pad($latest_end_minute, 2, '0', STR_PAD_LEFT) . ':00', $tz_obj);
            $filtered = array();
            foreach ($times as $slot) {
                try {
                    $start  = new DateTime($slot['start_at']);
                    $start->setTimezone($tz_obj);
                    $end    = clone $start;
                    $end->modify("+{$duration_min} minutes");
                    if ($end <= $cutoff) {
                        $filtered[] = $slot;
                    }
                } catch (Exception $e) {
                    $filtered[] = $slot; // keep on parse error
                }
            }
            return $filtered;
        };

        // -- Multi-staff intersection: when multiple staff are selected for a
        //    multi-service appointment, only show times ALL staff are free ------
        $all_staff_ids_raw = isset($_POST['all_staff_ids']) ? stripslashes($_POST['all_staff_ids']) : '';
        $all_staff_ids     = array();
        if (!empty($all_staff_ids_raw)) {
            $decoded = json_decode($all_staff_ids_raw, true);
            if (is_array($decoded) && count($decoded) > 1) {
                $all_staff_ids = array_map('intval', $decoded);
            }
        }
        // Also parse all_option_ids per staff (parallel array of option_ids JSON strings)
        $all_option_ids_raw = isset($_POST['all_option_ids']) ? stripslashes($_POST['all_option_ids']) : '';
        $all_option_ids_arr = array(); // array of arrays, one per staff
        if (!empty($all_option_ids_raw)) {
            $decoded = json_decode($all_option_ids_raw, true);
            if (is_array($decoded)) {
                $all_option_ids_arr = $decoded;
            }
        }

        // Per-staff service IDs - fallback when option variation lookup returns empty
        $all_service_ids_raw = isset($_POST['all_service_ids']) ? stripslashes($_POST['all_service_ids']) : '';
        $all_service_ids_arr = array(); // array of service_ids, one per staff (same index as all_staff_ids)
        if (!empty($all_service_ids_raw)) {
            $decoded = json_decode($all_service_ids_raw, true);
            if (is_array($decoded)) {
                $all_service_ids_arr = array_map('intval', $decoded);
            }
        }

        // Guest labels distinguish sequential services for one guest from
        // simultaneous services for guests booking together.
        $all_guest_labels_raw = isset($_POST['all_guest_labels']) ? stripslashes($_POST['all_guest_labels']) : '';
        $all_guest_labels_arr = array();
        if (!empty($all_guest_labels_raw)) {
            $decoded = json_decode($all_guest_labels_raw, true);
            if (is_array($decoded)) {
                $all_guest_labels_arr = array_map('sanitize_text_field', $decoded);
            }
        }

        $all_durations_raw = isset($_POST['all_durations']) ? stripslashes($_POST['all_durations']) : '';
        $all_durations_arr = array();
        if (!empty($all_durations_raw)) {
            $decoded = json_decode($all_durations_raw, true);
            if (is_array($decoded)) {
                $all_durations_arr = array_map('intval', $decoded);
            }
        }

        // Parse selected_option_ids early (also used in multi-staff block below)
        $selected_option_ids = array();
        if (!empty($_POST['option_ids'])) {
            $raw_opts = json_decode(stripslashes($_POST['option_ids']), true);
            if (is_array($raw_opts)) {
                $selected_option_ids = array_map('intval', $raw_opts);
            }
        }

        // -- Parse cart busy-staff windows sent from frontend -----------------
        // Format: [{ staff_id: int, start_at: ISO-string, duration: int-minutes }, ...]
        // Used to exclude already-occupied named staff from Any-staff candidate pools.
        $cart_busy_windows = array(); // staff_id => [ [startMs, endMs], ... ]
        if (!empty($_POST['cart_busy_staff'])) {
            $raw_busy = json_decode(stripslashes($_POST['cart_busy_staff']), true);
            if (is_array($raw_busy)) {
                foreach ($raw_busy as $w) {
                    $sid = isset($w['staff_id']) ? intval($w['staff_id']) : 0;
                    $dur = isset($w['duration']) ? intval($w['duration']) : 0;
                    if ($sid <= 0 || empty($w['start_at'])) continue;
                    try {
                        $wStart = new DateTime($w['start_at']);
                        $wEnd   = clone $wStart;
                        $wEnd->modify("+{$dur} minutes");
                        if (!isset($cart_busy_windows[$sid])) $cart_busy_windows[$sid] = array();
                        $cart_busy_windows[$sid][] = array(
                            'startTs' => $wStart->getTimestamp(),
                            'endTs'   => $wEnd->getTimestamp(),
                        );
                    } catch (Exception $e) {}
                }
            }
        }

        /**
         * Check if a staff member is busy (has a cart appointment) during a given window.
         * @param int $staff_id
         * @param int $slotStartTs  Unix timestamp of candidate slot start
         * @param int $durationMin  Duration of the service in minutes
         * @return bool
         */
        $is_staff_cart_busy = function($staff_id, $slotStartTs, $durationMin) use ($cart_busy_windows) {
            if (empty($cart_busy_windows[$staff_id])) return false;
            $wEnd = $slotStartTs + $durationMin * 60;
            foreach ($cart_busy_windows[$staff_id] as $iv) {
                if ($slotStartTs < $iv['endTs'] && $wEnd > $iv['startTs']) return true;
            }
            return false;
        };

        if (!$service_id || !$date) {
            wp_send_json_error(array('message' => __('Missing required parameters', 'yatoon-booking-system')));
        }

        // -- Shared: reject dates in the past ---------------------------------
        $timezone_str_early = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz_now_early       = new DateTimeZone($timezone_str_early);
        $now_early          = new DateTime('now', $tz_now_early);
        $today_str_early    = $now_early->format('Y-m-d');

        if ($date < $today_str_early) {
            wp_send_json_error(array(
                'message'         => __('This date has already passed.', 'yatoon-booking-system'),
                'no_availability' => true,
            ));
        }

        // Route based on active platform
        $platform = sanitize_key((string) get_option('yatoon_active_platform', 'local'));
        if (!in_array($platform, array('local', 'square', 'vagaro'), true)) {
            $platform = 'local';
        }

        $external_closed_day_message = function() use ($platform, $date, $timezone) {
            static $checked = false;
            static $message = null;

            if ($checked) {
                return $message;
            }
            $checked = true;

            $closed_text = __('No online times are available for this date. We may be closed or fully booked. Please choose another date or contact us.', 'yatoon-booking-system');

            if ($platform === 'square' && is_object($this->square_api) && method_exists($this->square_api, 'is_location_closed_on')) {
                $square_closed = $this->square_api->is_location_closed_on($date, $timezone);
                if ($square_closed === true) {
                    $message = $closed_text;
                    return $message;
                }
            }

            if (in_array($platform, array('square', 'vagaro'), true)
                && class_exists('YATOON_Business_Hours')
                && get_option('yatoon_external_closed_day_fallback', '1') === '1'
                && YATOON_Business_Hours::get_day_hours($date) === false
            ) {
                $message = $closed_text;
            }

            return $message;
        };

        // -- VAGARO MODE -------------------------------------------------------
        if ($platform === 'vagaro') {
            if (!class_exists('YATOON_Vagaro_API')) {
                require_once SBS_PLUGIN_DIR . 'includes/class-yatoon-vagaro-api.php';
            }
            $vagaro = new YATOON_Vagaro_API();

            // Get the Vagaro service_id and employee_id for this booking
            global $wpdb;
            $svc = yatoon_db_get_row($wpdb->prepare(
                "SELECT square_id FROM {$wpdb->prefix}yatoon_services WHERE id = %d", $service_id
            )); // square_id column stores the external platform ID for all platforms
            $vagaro_service_id = $svc ? $svc->square_id : '';

            $vagaro_employee_id = null;
            if ($staff_id) {
                $stf = yatoon_db_get_row($wpdb->prepare(
                    "SELECT square_id FROM {$wpdb->prefix}yatoon_staff WHERE id = %d", $staff_id
                ));
                if ($stf) $vagaro_employee_id = $stf->square_id;
            }

            if (empty($vagaro_service_id)) {
                wp_send_json_error(array(
                    'message'         => __('Unable to check availability for this service. Please call us to book.', 'yatoon-booking-system'),
                    'no_availability' => true,
                ));
            }

            $resp = $vagaro->get_availability($vagaro_service_id, $vagaro_employee_id, $date);

            if (is_wp_error($resp)) {
                wp_send_json_error(array(
                    'message'         => ($external_closed_day_message() ?: __('No available times on this date.', 'yatoon-booking-system')),
                    'no_availability' => true,
                ));
            }

            // Vagaro returns times in response - normalise to our format
            $timezone   = ( wp_timezone_string() ?: 'America/Chicago' );
            $times      = array();
            $slots_raw  = $resp['slots'] ?? $resp['availabilities'] ?? $resp['times'] ?? array();

            foreach ($slots_raw as $slot) {
                $time_str = $slot['startTime'] ?? $slot['start_at'] ?? $slot['time'] ?? '';
                if (!$time_str) continue;
                try {
                    $t = new DateTime($time_str);
                    $t->setTimezone(new DateTimeZone($timezone));
                    $times[] = array(
                        'time'     => $t->format('H:i'),
                        'display'  => $t->format('g:i A'),
                        'start_at' => $time_str,
                    );
                } catch (Exception $e) {}
            }

            if (empty($times)) {
                wp_send_json_error(array(
                    'message'         => ($external_closed_day_message() ?: __('No available times on this date.', 'yatoon-booking-system')),
                    'no_availability' => true,
                ));
            }

            wp_send_json_success(array(
                'times' => $times,
                'count' => count($times),
                'mode'  => 'vagaro',
            ));
        }

        // -- LOCAL MODE --------------------------------------------------------
        if ($platform === 'local') {
            $day_hours_local = YATOON_Business_Hours::get_day_hours($date);
            if (!$day_hours_local) {
                wp_send_json_error(array(
                    'message'         => __('We are closed on this day. Please choose another date.', 'yatoon-booking-system'),
                    'no_availability' => true,
                ));
            }

	            $local_duration_override = ($total_duration_hint > 0 && empty($all_service_ids_arr)) ? $total_duration_hint : null;
	            $slots = YATOON_Business_Hours::generate_slots($date, $service_id, $staff_id, $local_duration_override);

            if (!empty($all_staff_ids) && count($all_staff_ids) > 1 && !empty($all_service_ids_arr)) {
                global $wpdb;
                $local_tz_obj = new DateTimeZone($timezone ?: 'America/New_York');
                $table_staff_services = $wpdb->prefix . 'yatoon_staff_services';
                $table_staff = $wpdb->prefix . 'yatoon_staff';
                $table_bookings = $wpdb->prefix . 'yatoon_bookings';

                $duration_for_service = function($idx, $svc_id) use ($wpdb, $all_durations_arr) {
                    if (isset($all_durations_arr[$idx]) && intval($all_durations_arr[$idx]) > 0) {
                        return max(1, intval($all_durations_arr[$idx]));
                    }
                    $svc_dur = yatoon_db_get_var($wpdb->prepare(
                        "SELECT duration_minutes FROM {$wpdb->prefix}yatoon_services WHERE id = %d LIMIT 1",
                        intval($svc_id)
                    ));
                    return max(1, intval($svc_dur ?: 60));
                };

                $eligible_local_staff = function($svc_id, $selected_staff_id) use ($wpdb, $table_staff_services, $table_staff) {
                    $selected_staff_id = intval($selected_staff_id);
                    if ($selected_staff_id > 0) {
                        return array($selected_staff_id);
                    }
                    $assigned = yatoon_db_get_col($wpdb->prepare(
                        "SELECT s.id
                           FROM $table_staff s
                           INNER JOIN $table_staff_services ss ON s.id = ss.staff_id
                          WHERE ss.service_id = %d
                            AND s.is_active = 1
                            AND s.is_bookable = 1
                          ORDER BY s.sort_order ASC, s.name ASC",
                        intval($svc_id)
                    ));
                    if (empty($assigned)) {
                        $assigned = yatoon_db_get_col(
                            "SELECT id FROM $table_staff WHERE is_active = 1 AND is_bookable = 1 ORDER BY sort_order ASC, name ASC"
                        );
                    }
                    return array_map('intval', (array) $assigned);
                };

                $staff_window_is_free = function($staff_id, DateTime $win_start, DateTime $win_end, array $assigned_windows) use ($wpdb, $table_bookings, $local_tz_obj) {
                    $slot_date = $win_start->format('Y-m-d');
                    $day_hours = YATOON_Business_Hours::get_day_hours($slot_date);
                    if (!$day_hours) {
                        return false;
                    }

                    $open_dt  = new DateTime($slot_date . ' ' . ($day_hours['start'] ?? '09:00'), $local_tz_obj);
                    $close_dt = new DateTime($slot_date . ' ' . ($day_hours['end'] ?? '17:00'), $local_tz_obj);
                    if ($win_start < $open_dt || $win_end > $close_dt) {
                        return false;
                    }

                    if (!empty($day_hours['break_start']) && !empty($day_hours['break_end'])) {
                        $break_start = new DateTime($slot_date . ' ' . $day_hours['break_start'], $local_tz_obj);
                        $break_end   = new DateTime($slot_date . ' ' . $day_hours['break_end'], $local_tz_obj);
                        if ($win_start < $break_end && $win_end > $break_start) {
                            return false;
                        }
                    }

                    foreach ($assigned_windows as $assigned) {
                        if (intval($assigned['staff_id']) === intval($staff_id) && $win_start < $assigned['end'] && $win_end > $assigned['start']) {
                            return false;
                        }
                    }

                    $conflict = yatoon_db_get_var($wpdb->prepare(
                        "SELECT id FROM $table_bookings
                          WHERE staff_id = %d
                            AND status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')
                            AND start_time < %s
                            AND end_time > %s
                          LIMIT 1",
                        intval($staff_id),
                        $win_end->format('Y-m-d H:i:s'),
                        $win_start->format('Y-m-d H:i:s')
                    ));
                    if ($conflict) {
                        return false;
                    }

                    return !YATOON_Business_Hours::has_staff_time_off_conflict(intval($staff_id), $win_start, $win_end, $local_tz_obj);
                };

                $segments = array();
                $guest_offsets = array();
                foreach ($all_service_ids_arr as $idx => $svc_id_for_segment) {
                    $svc_id_for_segment = intval($svc_id_for_segment);
                    if ($svc_id_for_segment <= 0) {
                        continue;
                    }
                    $guest_label = isset($all_guest_labels_arr[$idx]) && $all_guest_labels_arr[$idx] !== '' ? $all_guest_labels_arr[$idx] : 'Me';
                    $offset = isset($guest_offsets[$guest_label]) ? intval($guest_offsets[$guest_label]) : 0;
                    $duration = $duration_for_service($idx, $svc_id_for_segment);
                    $guest_offsets[$guest_label] = $offset + $duration;
                    $segments[] = array(
                        'service_id' => $svc_id_for_segment,
                        'staff_id'   => isset($all_staff_ids[$idx]) ? intval($all_staff_ids[$idx]) : 0,
                        'offset'     => $offset,
                        'duration'   => $duration,
                    );
                }

                $whole_cart_slot_is_available = function($slot) use ($segments, $eligible_local_staff, $staff_window_is_free, $local_tz_obj) {
                    if (empty($segments)) {
                        return true;
                    }
                    try {
                        $slot_start = new DateTime($slot['start_at']);
                        $slot_start->setTimezone($local_tz_obj);
                    } catch (Exception $e) {
                        return false;
                    }

                    $windows = array();
                    foreach ($segments as $segment) {
                        $win_start = clone $slot_start;
                        if ($segment['offset'] > 0) {
                            $win_start->modify('+' . intval($segment['offset']) . ' minutes');
                        }
                        $win_end = clone $win_start;
                        $win_end->modify('+' . intval($segment['duration']) . ' minutes');
                        $candidates = $eligible_local_staff($segment['service_id'], $segment['staff_id']);
                        if (empty($candidates)) {
                            return false;
                        }
                        $windows[] = array(
                            'start'      => $win_start,
                            'end'        => $win_end,
                            'candidates' => $candidates,
                        );
                    }

                    $try_assign = function($idx, $assigned_windows) use (&$try_assign, $windows, $staff_window_is_free) {
                        if ($idx >= count($windows)) {
                            return true;
                        }
                        foreach ($windows[$idx]['candidates'] as $candidate_staff_id) {
                            if (!$staff_window_is_free($candidate_staff_id, $windows[$idx]['start'], $windows[$idx]['end'], $assigned_windows)) {
                                continue;
                            }
                            $next_assigned = $assigned_windows;
                            $next_assigned[] = array(
                                'staff_id' => intval($candidate_staff_id),
                                'start'    => $windows[$idx]['start'],
                                'end'      => $windows[$idx]['end'],
                            );
                            if ($try_assign($idx + 1, $next_assigned)) {
                                return true;
                            }
                        }
                        return false;
                    };

                    return $try_assign(0, array());
                };

                $slots = array_values(array_filter($slots, $whole_cart_slot_is_available));
            }
            if (empty($slots)) {
                $msg = ($date === $today_str_early)
                    ? __('No more available times today. Please choose another date.', 'yatoon-booking-system')
                    : __('No available times on this date. All slots may be booked.', 'yatoon-booking-system');
                wp_send_json_error(array('message' => $msg, 'no_availability' => true));
            }

            wp_send_json_success(array(
                'times' => $slots,
                'count' => count($slots),
                'mode'  => 'local',
            ));
        }

        // -- SQUARE MODE below - local business hours are NOT consulted --------

        // ------------------------------------------------------------
        // MULTI-STAFF INTERSECTION MODE (including Any Staff expansion)
        //
        // Handles two scenarios:
        // A) All staff are named -> find times ALL are free simultaneously
        // B) Some staff = "Any staff" (id=0) -> expand to all eligible staff,
        //    find a valid assignment where all services can be covered at once.
        //    e.g. Staff A is booked for svc1, "Any staff" for svc2:
        //    -> check if that staff (or any other available staff) can cover svc2
        //      at the same start time AFTER finishing svc1.
        //    Square handles sequential booking natively - if the staff has a slot
        //    at T that's long enough for BOTH services, Square will show it.
        //    But if the staff is only free for svc1's duration, the slot won't appear
        //    for svc2 - so "Any staff" must be resolved to a real staff member
        //    who has enough time, not just whoever Square returns for "null".
        // ------------------------------------------------------------
        if (!empty($all_staff_ids) && count($all_staff_ids) > 1) {
            global $wpdb;
            $table_staff_ms    = $wpdb->prefix . 'yatoon_staff';
            $table_options_ms  = $wpdb->prefix . 'yatoon_service_options';
            $table_ss_ms       = $wpdb->prefix . 'yatoon_staff_services';

            $timezone_ms   = ( wp_timezone_string() ?: 'America/Chicago' );
            $tz_obj_ms     = new DateTimeZone($timezone_ms);
            $date_start_ms = (new DateTime($date . ' 00:00:00', $tz_obj_ms))->format('c');
            $date_end_ms   = (new DateTime($date . ' 23:59:59', $tz_obj_ms))->format('c');

            // -- Helper: fetch Square slots for one staff + one variation list -
            $get_staff_slots = function($sq_staff_id, $variations, $date_s, $date_e) use ($tz_obj_ms) {
                $slots = array();
                foreach ($variations as $var_id) {
                    $resp = $this->square_api->search_availability($var_id, $date_s, $date_e, $sq_staff_id);
                    if (is_wp_error($resp) || !isset($resp['availabilities'])) continue;
                    foreach ($resp['availabilities'] as $av) {
                        if (empty($av['start_at'])) continue;
                        $t = new DateTime($av['start_at']);
                        $t->setTimezone($tz_obj_ms);
                        $key = $t->format('H:i');
                        if (!isset($slots[$key])) $slots[$key] = $av['start_at'];
                    }
                    if (!empty($slots)) break;
                }
                return $slots;
            };

            // -- Helper: get variation square_ids for a list of option DB IDs -
            $get_variations = function($opt_ids, $fallback_service_id) use ($wpdb, $table_options_ms) {
                $vars = array();
                if (!empty($opt_ids)) {
                    $clean = array_map('intval', (array)$opt_ids);
                    $ph    = implode(',', array_fill(0, count($clean), '%d'));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $rows  = yatoon_db_get_results(
                        $wpdb->prepare("SELECT square_id FROM $table_options_ms WHERE id IN ($ph) AND square_id != ''", ...$clean)
                    );
                    foreach ($rows as $r) $vars[] = $r->square_id;
                }
                if (empty($vars) && $fallback_service_id) {
                    $rows = yatoon_db_get_results($wpdb->prepare(
                        "SELECT square_id FROM $table_options_ms WHERE service_id = %d AND square_id != '' LIMIT 3",
                        $fallback_service_id
                    ));
                    foreach ($rows as $r) $vars[] = $r->square_id;
                }
                return $vars;
            };

            // -- Helper: get all active bookable staff who can do a service ----
            // First tries yatoon_staff_services for precise assignment; if the table has no
            // entries for this service (e.g. after a sync that missed some services),
            // falls back to ALL active bookable staff so availability is never blocked by
            // a missing DB row - Square's own availability API is the true gatekeeper.
            $get_eligible_staff = function($svc_id) use ($wpdb, $table_staff_ms, $table_ss_ms) {
                $assigned = yatoon_db_get_results($wpdb->prepare("
                    SELECT s.id, s.square_id, s.name
                    FROM $table_staff_ms s
                    INNER JOIN $table_ss_ms ss ON s.id = ss.staff_id
                    WHERE s.is_active = 1 AND s.is_bookable = 1 AND ss.service_id = %d
                    ORDER BY s.sort_order ASC, s.name ASC
                ", $svc_id));
                if (!empty($assigned)) return $assigned;
                // Fallback: yatoon_staff_services has no rows for this service.
                // Return all bookable staff - Square's availability API will filter
                // to only those who actually have the service in their booking profile.
                yatoon_safe_log("SBS get_eligible_staff: yatoon_staff_services empty for service_id=$svc_id - falling back to all bookable staff");
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                return yatoon_db_get_results(
                    "SELECT id, square_id, name FROM $table_staff_ms WHERE is_active=1 AND is_bookable=1 ORDER BY sort_order ASC, name ASC"
                );
            };

            $get_position_duration = function($opt_ids, $fallback_service_id) use ($wpdb) {
                $opt_ids = is_array($opt_ids) ? $opt_ids : array();
                if (!empty($opt_ids)) {
                    $opt_dur = yatoon_db_get_var($wpdb->prepare(
                        "SELECT duration_minutes FROM {$wpdb->prefix}yatoon_service_options WHERE id = %d LIMIT 1",
                        intval($opt_ids[0])
                    ));
                    if ($opt_dur) {
                        return max(1, intval($opt_dur));
                    }
                }

                $svc_dur = yatoon_db_get_var($wpdb->prepare(
                    "SELECT duration_minutes FROM {$wpdb->prefix}yatoon_services WHERE id = %d LIMIT 1",
                    intval($fallback_service_id)
                ));

                return $svc_dur ? max(1, intval($svc_dur)) : 60;
            };

            // -- Check if any staff_id = 0 (Any staff) in the list -------------
            $has_any_staff = in_array(0, $all_staff_ids, true);

            if (!$has_any_staff) {
                // -------------------------------------------------------------
                // CASE A: All staff are explicitly named.
                // Group services by staff member, then query each staff with ALL
                // their services as one multi-segment request so Square validates
                // the combined duration (e.g. Staff: 40min + 30min = 70min)
                // against her actual availability window in a single API call.
                // -------------------------------------------------------------

                // Build per-guest segment lists. Each guest's services are sequential,
                // while separate guests must share the same appointment start time.
                $guest_segments = array(); // guest_label => [segment, ...]
                $guest_offsets  = array();
                $staff_windows  = array();
                foreach ($all_staff_ids as $s_idx => $s_id) {
                    $s_row = yatoon_db_get_row($wpdb->prepare("SELECT square_id FROM $table_staff_ms WHERE id = %d", $s_id));
                    if (!$s_row || empty($s_row->square_id)) continue;
                    $s_opts = isset($all_option_ids_arr[$s_idx]) ? (array)$all_option_ids_arr[$s_idx] : array();
                    $s_fallback_svc = isset($all_service_ids_arr[$s_idx]) && $all_service_ids_arr[$s_idx] > 0
                        ? $all_service_ids_arr[$s_idx]
                        : $service_id;
                    $s_vars = $get_variations($s_opts, $s_fallback_svc);
                    if (empty($s_vars)) continue;

                    $guest_label = isset($all_guest_labels_arr[$s_idx]) && $all_guest_labels_arr[$s_idx] !== ''
                        ? $all_guest_labels_arr[$s_idx]
                        : 'Me';
                    if (!isset($guest_offsets[$guest_label])) {
                        $guest_offsets[$guest_label] = 0;
                    }
                    $pos_offset   = intval($guest_offsets[$guest_label]);
                    $pos_duration = $get_position_duration($s_opts, $s_fallback_svc);
                    $guest_offsets[$guest_label] += $pos_duration;

                    if (!isset($staff_windows[$s_id])) {
                        $staff_windows[$s_id] = array();
                    }
                    foreach ($staff_windows[$s_id] as $window) {
                        if ($pos_offset < $window['end'] && ($pos_offset + $pos_duration) > $window['start']) {
                            wp_send_json_error(array(
                                'message'         => __('One staff member cannot serve multiple guests at the same start time. Please choose different staff or Any staff.', 'yatoon-booking-system'),
                                'no_availability' => true,
                            ));
                        }
                    }
                    $staff_windows[$s_id][] = array(
                        'start' => $pos_offset,
                        'end'   => $pos_offset + $pos_duration,
                    );

                    if (!isset($guest_segments[$guest_label])) {
                        $guest_segments[$guest_label] = array();
                    }
                    foreach ($s_vars as $v) {
                        $guest_segments[$guest_label][] = array(
                            'variation_id'   => $v,
                            'team_member_id' => $s_row->square_id,
                        );
                    }
                }

                if (!empty($guest_segments)) {
                    $times_per_guest = array();
                    foreach ($guest_segments as $guest_label => $segments) {
                        $resp  = $this->square_api->search_availability_multi_segment($segments, $date_start_ms, $date_end_ms);
                        $slots = array();
                        if (!is_wp_error($resp) && isset($resp['availabilities'])) {
                            foreach ($resp['availabilities'] as $av) {
                                if (empty($av['start_at'])) continue;
                                $t = new DateTime($av['start_at']);
                                $t->setTimezone($tz_obj_ms);
                                $key = $t->format('H:i');
                                if (!isset($slots[$key])) $slots[$key] = $av['start_at'];
                            }
                        }
                        yatoon_safe_log("SBS Multi-Guest(A multi-seg) - guest=$guest_label segs=" . count($segments) . " slots=" . count($slots));
                        $times_per_guest[$guest_label] = $slots;
                    }

                    // Intersect across guests so every guest can begin together.
                    $intersection = reset($times_per_guest);
                    foreach ($times_per_guest as $slots) {
                        $intersection = array_intersect_key($intersection, $slots);
                    }
                    ksort($intersection);

                    if (empty($intersection)) {
                        wp_send_json_error(array(
                            'message'         => ($external_closed_day_message() ?: __('We could not find a time when all selected staff are available together.', 'yatoon-booking-system')),
                            'no_availability' => true,
                        ));
                    }

                    $final_times = array();
                    foreach ($intersection as $key => $start_at) {
                        $t = new DateTime($start_at);
                        $t->setTimezone($tz_obj_ms);
                        $final_times[] = array('time' => $key, 'display' => $t->format('g:i A'), 'start_at' => $start_at);
                    }
                    $final_times = $apply_cutoff($final_times, $total_duration_hint, $date);
                    if (empty($final_times)) {
                        wp_send_json_error(array('message' => __('No available times - all remaining slots would end after our closing time.', 'yatoon-booking-system'), 'no_availability' => true));
                    }
                    wp_send_json_success(array('times' => $final_times, 'count' => count($final_times), 'mode' => 'square_multi_staff'));
                }
                // fall through to single-staff query if no staff_segments

            } else {
                // -------------------------------------------------------------
                // CASE B: Mixed - some staff named, at least one is "Any staff"
                //
                // SEQUENTIAL MODEL (mirrors Square's own logic):
                // Services run one after another in booking order. For booking
                // start time T:
                //   - Position 0 runs  T -> T+dur0          (staffA)
                //   - Position 1 runs  T+dur0 -> T+dur0+dur1 (staffB or staffA)
                //   - etc.
                //
                // For "Any staff" positions we enumerate all eligible staff
                // combinations and use Square's multi-segment API with the FULL
                // ordered segment list (named + any, in position order). Square
                // validates each staff's window at the correct sequential offset.
                //
                // This is exactly how Square's widget works - e.g. Dipping Powder
                // 11:15 (Staff A, 35 min) + Acrylic Fill-in 12:00 (Staff B, 40 min) ->
                // shows 11:15 as a valid slot.
                // -------------------------------------------------------------

                // Step 1: Parse each position - named vs any-staff
                // ordered_positions[s_idx] = ['type' => 'named'|'any', 'staff_id'=>...,
                //   'sq_id'=>..., 'variation_id'=>..., 'duration'=>..., 'candidates'=>...]
                $ordered_positions = array();
                $has_named = false;

                foreach ($all_staff_ids as $s_idx => $s_id) {
                    $s_opts = isset($all_option_ids_arr[$s_idx]) ? (array)$all_option_ids_arr[$s_idx] : array();
                    $s_fallback_svc = isset($all_service_ids_arr[$s_idx]) && $all_service_ids_arr[$s_idx] > 0
                        ? $all_service_ids_arr[$s_idx]
                        : $service_id;
                    $s_vars = $get_variations($s_opts, $s_fallback_svc);
                    $var_id = !empty($s_vars) ? $s_vars[0] : null;

                    // Get duration for this position
                    $pos_duration = 0;
                    if (!empty($s_opts)) {
                        $opt_dur = yatoon_db_get_var($wpdb->prepare(
                            "SELECT duration_minutes FROM {$wpdb->prefix}yatoon_service_options WHERE id = %d LIMIT 1",
                            intval($s_opts[0])
                        ));
                        if ($opt_dur) $pos_duration = intval($opt_dur);
                    }
                    if (!$pos_duration) {
                        $svc_dur_row = yatoon_db_get_row($wpdb->prepare(
                            "SELECT duration_minutes FROM {$wpdb->prefix}yatoon_services WHERE id = %d", $s_fallback_svc
                        ));
                        if ($svc_dur_row) $pos_duration = intval($svc_dur_row->duration_minutes);
                    }
                    if (!$pos_duration) $pos_duration = 60;

                    if ($s_id === 0) {
                        // Any staff position
                        $svc_for_any = $s_fallback_svc;
                        if (!empty($s_opts)) {
                            $opt_svc = yatoon_db_get_var($wpdb->prepare(
                                "SELECT service_id FROM {$wpdb->prefix}yatoon_service_options WHERE id = %d LIMIT 1",
                                intval($s_opts[0])
                            ));
                            if ($opt_svc) $svc_for_any = intval($opt_svc);
                        }
                        $candidates = $get_eligible_staff($svc_for_any);
                        $ordered_positions[$s_idx] = array(
                            'type'       => 'any',
                            'variation'  => $var_id,
                            'duration'   => $pos_duration,
                            'candidates' => $candidates,
                            'service_id' => $svc_for_any,
                        );
                    } else {
                        // Named staff position
                        $s_row = yatoon_db_get_row($wpdb->prepare(
                            "SELECT id, square_id, name FROM {$wpdb->prefix}yatoon_staff WHERE id = %d", $s_id
                        ));
                        if (!$s_row || empty($s_row->square_id) || !$var_id) continue;
                        $has_named = true;
                        $ordered_positions[$s_idx] = array(
                            'type'      => 'named',
                            'staff_id'  => $s_id,
                            'sq_id'     => $s_row->square_id,
                            'name'      => $s_row->name,
                            'variation' => $var_id,
                            'duration'  => $pos_duration,
                        );
                    }
                }

                if (empty($ordered_positions)) {
                    wp_send_json_error(array('message' => __('No valid services found.', 'yatoon-booking-system'), 'no_availability' => true));
                }

                // Step 2: Pre-compute start times for each position.
                // Same guest = sequential services. Different guests = same start time.
                // and filter out candidates who have DB conflicts at their actual time window.
                $guest_offsets = array();
                foreach ($ordered_positions as $s_idx => $pos) {
                    if ($pos['type'] === 'any') {
                        // Compute actual start/end for this position
                        $pos_start = clone (new DateTime($date . ' 00:00:00', $tz_obj_ms));
                        // We don't know exact start until user picks a time, so we use
                        // the DB check only at booking time (Pass2). At Step 4 we trust Square.
                        // The key fix is: don't allow same staff twice in simultaneous positions.
                        // For sequential (non-overlapping) positions, same staff is OK.
                    }
                    $guest_label = isset($all_guest_labels_arr[$s_idx]) && $all_guest_labels_arr[$s_idx] !== ''
                        ? $all_guest_labels_arr[$s_idx]
                        : 'Me';
                    if (!isset($guest_offsets[$guest_label])) {
                        $guest_offsets[$guest_label] = 0;
                    }
                    $ordered_positions[$s_idx]['guest_label'] = $guest_label;
                    $ordered_positions[$s_idx]['cum_offset']  = intval($guest_offsets[$guest_label]);
                    $guest_offsets[$guest_label] += intval($pos['duration'] ?? 60);
                }

                // Build all candidate combinations to query
                // For named positions, the staff is fixed.
                // For any-positions, enumerate candidates.
                // Separate into any-positions array (ordered by s_idx)
                $any_idxs = array();
                foreach ($ordered_positions as $s_idx => $pos) {
                    if ($pos['type'] === 'any') $any_idxs[] = $s_idx;
                }

                // Build combination arrays for any-positions
                // Each combination: [s_idx => candidate_object, ...]
                $combinations = array(array()); // start with one empty combination
                foreach ($any_idxs as $s_idx) {
                    $pos = $ordered_positions[$s_idx];
                    $pos_offset = $pos['cum_offset'] ?? 0;
                    $new_combos = array();
                    foreach ($combinations as $combo) {
                        foreach ($pos['candidates'] as $cand) {
                            if (empty($cand->square_id)) continue;
                            // For two any-positions at the SAME offset (simultaneous),
                            // do NOT allow the same staff twice.
                            // For sequential positions (different offsets), same staff is OK.
                            $conflict_with_same_offset = false;
                            foreach ($combo as $prev_idx => $prev_cand) {
                                $prev_offset = $ordered_positions[$prev_idx]['cum_offset'] ?? -1;
                                if ($prev_offset === $pos_offset && $prev_cand->square_id === $cand->square_id) {
                                    $conflict_with_same_offset = true;
                                    break;
                                }
                            }
                            if ($conflict_with_same_offset) continue;
                            $new_combo = $combo;
                            $new_combo[$s_idx] = $cand;
                            $new_combos[] = $new_combo;
                        }
                    }
                    $combinations = $new_combos;
                }

                // Deduplicate combinations that would produce identical API queries
                // (same staff assignment for all positions in same order)
                $combo_keys_seen = array();
                $unique_combos = array();
                foreach ($combinations as $combo) {
                    $key_parts = array();
                    foreach ($ordered_positions as $s_idx => $pos) {
                        if ($pos['type'] === 'named') {
                            $key_parts[] = 'n' . $pos['sq_id'];
                        } else {
                            $cand = isset($combo[$s_idx]) ? $combo[$s_idx] : null;
                            $key_parts[] = 'a' . ($cand ? $cand->square_id : '?');
                        }
                    }
                    $ckey = implode('|', $key_parts);
                    if (!isset($combo_keys_seen[$ckey])) {
                        $combo_keys_seen[$ckey] = true;
                        $unique_combos[] = $combo;
                    }
                }

                // Step 3: For each unique combination, query Square per guest.
                // Each guest's services are sequential; different guests start together.
                $valid_slots           = array(); // 'H:i' => start_at
                $valid_slots_staff_map = array(); // 'H:i' => [s_idx => sq_staff_id]

                foreach ($unique_combos as $combo) {
                    $guest_segments = array();
                    $staff_windows  = array();
                    $slot_staff     = array();
                    $valid_combo    = true;

                    foreach ($ordered_positions as $s_idx => $pos) {
                        if (!isset($pos['variation']) || !$pos['variation']) {
                            $valid_combo = false; break;
                        }
                        if ($pos['type'] === 'named') {
                            $sq_staff_id = $pos['sq_id'];
                        } else {
                            $cand = isset($combo[$s_idx]) ? $combo[$s_idx] : null;
                            if (!$cand || empty($cand->square_id)) { $valid_combo = false; break; }
                            $sq_staff_id = $cand->square_id;
                        }

                        $offset   = intval($pos['cum_offset'] ?? 0);
                        $duration = intval($pos['duration'] ?? 60);
                        if (!isset($staff_windows[$sq_staff_id])) {
                            $staff_windows[$sq_staff_id] = array();
                        }
                        foreach ($staff_windows[$sq_staff_id] as $window) {
                            if ($offset < $window['end'] && ($offset + $duration) > $window['start']) {
                                $valid_combo = false;
                                break 2;
                            }
                        }
                        $staff_windows[$sq_staff_id][] = array('start' => $offset, 'end' => $offset + $duration);

                        $guest_label = isset($pos['guest_label']) && $pos['guest_label'] !== '' ? $pos['guest_label'] : 'Me';
                        if (!isset($guest_segments[$guest_label])) {
                            $guest_segments[$guest_label] = array();
                        }
                        $guest_segments[$guest_label][] = array(
                            'variation_id'   => $pos['variation'],
                            'team_member_id' => $sq_staff_id,
                        );
                        $slot_staff[$s_idx] = $sq_staff_id;
                    }

                    if (!$valid_combo || empty($guest_segments)) continue;

                    $combo_intersection = null;
                    foreach ($guest_segments as $guest_label => $segments) {
                        $resp = $this->square_api->search_availability_multi_segment($segments, $date_start_ms, $date_end_ms);
                        if (is_wp_error($resp) || !isset($resp['availabilities'])) {
                            $combo_intersection = array();
                            break;
                        }

                        $guest_slots = array();
                        foreach ($resp['availabilities'] as $av) {
                            if (empty($av['start_at'])) continue;
                            $t = new DateTime($av['start_at']);
                            $t->setTimezone($tz_obj_ms);
                            $guest_slots[$t->format('H:i')] = $av['start_at'];
                        }

                        $combo_intersection = ($combo_intersection === null)
                            ? $guest_slots
                            : array_intersect_key($combo_intersection, $guest_slots);
                        if (empty($combo_intersection)) break;
                    }

                    if (empty($combo_intersection)) continue;

                    foreach ($combo_intersection as $key => $start_at) {
                        // Always record this slot (from all combos) so ALL valid times are shown.
                        if (!isset($valid_slots[$key])) {
                            $valid_slots[$key] = $start_at;
                        }
                        if (!isset($valid_slots_staff_map[$key])) {
                            $valid_slots_staff_map[$key] = $slot_staff;
                        }
                    }
                }

                ksort($valid_slots);
                yatoon_safe_log("SBS Multi-Staff(B sequential) - combos=" . count($unique_combos) . " valid slots=" . count($valid_slots));

                if (empty($valid_slots)) {
                    wp_send_json_error(array(
                        'message'         => ($external_closed_day_message() ?: __('We could not find a time when all selected staff are available together.', 'yatoon-booking-system')),
                        'no_availability' => true,
                    ));
                }

                $final_times = array();
                foreach ($valid_slots as $key => $start_at) {
                    $t = new DateTime($start_at);
                    $t->setTimezone($tz_obj_ms);
                    $entry = array('time' => $key, 'display' => $t->format('g:i A'), 'start_at' => $start_at);
                    if (isset($valid_slots_staff_map[$key])) {
                        $entry['staff_map'] = $valid_slots_staff_map[$key];
                    }
                    $final_times[] = $entry;
                }
                $final_times = $apply_cutoff($final_times, $total_duration_hint, $date);
                if (empty($final_times)) {
                    wp_send_json_error(array('message' => __('No available times - all remaining slots would end after our closing time.', 'yatoon-booking-system'), 'no_availability' => true));
                }
                wp_send_json_success(array('times' => $final_times, 'count' => count($final_times), 'mode' => 'square_any_staff'));
            }
            // fall through to single-staff query if no valid data
        }

        // -- Real-time Square API availability query -----------------------------
        global $wpdb;
        $table_services = $wpdb->prefix . 'yatoon_services';
        $table_staff    = $wpdb->prefix . 'yatoon_staff';

        $service = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_services WHERE id = %d", $service_id));
        if (!$service) {
            wp_send_json_error(array('message' => __('Service not found', 'yatoon-booking-system')));
        }

        // CORRECT: yatoon_service_options.square_id = Square ITEM_VARIATION ID (needed for availability search)
        //          yatoon_services.square_id         = Square ITEM ID (parent, NOT usable for availability)
        $table_options = $wpdb->prefix . 'yatoon_service_options';

        // Parse the user-selected option IDs (sent from frontend Step 2)
        $selected_option_ids = array();
        if (!empty($_POST['option_ids'])) {
            $raw = json_decode(stripslashes($_POST['option_ids']), true);
            if (is_array($raw)) {
                $selected_option_ids = array_map('intval', $raw);
            }
        }

        // KEY FIX: If the user selected a specific option (e.g. "Acrylic Full Set" = 120 min),
        // we MUST query Square using ONLY that variation's ID.
        // Square's availability API uses the variation to determine required duration:
        // querying the wrong variation (e.g. "Acrylic Fill-in" = 40 min) returns slots that
        // don't have enough time for the actual service selected.
        //
        // Only fall back to "all variations for this service" when NO option was selected
        // (user didn't pick a sub-option, just the parent service).
        if (!empty($selected_option_ids)) {
            // User selected specific option(s) - use ONLY those variations.
            // ORDER BY sort_order ASC, id ASC ensures the primary (first) option comes first.
            $placeholders = implode(',', array_fill(0, count($selected_option_ids), '%d'));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $all_variations = yatoon_db_get_results(
                $wpdb->prepare(
                    "SELECT id, square_id, name, duration_minutes FROM $table_options WHERE id IN ($placeholders) AND square_id != '' ORDER BY sort_order ASC, id ASC",
                    ...$selected_option_ids
                )
            );
            yatoon_safe_log("SBS Availability Debug - Using SELECTED option variation(s): " . implode(', ', array_column((array)$all_variations, 'name')));
        } else {
            // No option selected - get the default/first variation for this service
            $all_variations = yatoon_db_get_results($wpdb->prepare(
                "SELECT id, square_id, name, duration_minutes FROM $table_options WHERE service_id = %d AND square_id != '' ORDER BY id ASC LIMIT 1",
                $service_id
            ));
            yatoon_safe_log("SBS Availability Debug - No option selected, using default variation: " . (!empty($all_variations) ? $all_variations[0]->name : 'NONE'));
        }

        $variation_id = !empty($all_variations) ? $all_variations[0]->square_id : null;

        // -- KEY FIX: single-service multi-option ------------------------------
        // When the user picks multiple options under the SAME service (e.g. "Acrylic Full Set"
        // + "Acrylic on Toe" both stored as options of one service), $all_variations contains
        // more than one variation.  Square's availability API MUST be called with ONLY ONE
        // variation - the PRIMARY one (longest duration = the main service).
        //
        // The "break on first result" loop below was designed for "try multiple candidate
        // variations, use whichever Square knows about". But when TWO semantically different
        // variations are present (main + add-on), whichever happens to be first in the DB
        // order gets queried first; if that's the add-on (short duration, possibly no Square
        // availability on its own), it gets 0 results, falls through to the main - BUT if the
        // add-on DOES have some Square slots (e.g. it's also a standalone service), the break
        // fires on it and the main is never queried, returning wrong/no results.
        //
        // Correct behaviour: sort by duration DESC so the primary (longest) variation is
        // always [0], query ONLY that one, and accumulate the remaining durations into
        // $addon_duration for the local secondary filter.
        if (count($all_variations) > 1) {
            // Sort by duration_minutes DESC - primary service always has the longest duration
            usort($all_variations, function($a, $b) {
                return intval($b->duration_minutes) - intval($a->duration_minutes);
            });
            $extra_duration = 0;
            foreach (array_slice((array)$all_variations, 1) as $extra_var) {
                $extra_duration += intval($extra_var->duration_minutes);
            }
            $addon_duration += $extra_duration;
            $all_variations = array($all_variations[0]); // only the primary (longest) variation
            yatoon_safe_log("SBS Availability Debug - multi-option same service: primary='{$all_variations[0]->name}' ({$all_variations[0]->duration_minutes}min), added {$extra_duration}min to addon_duration (now {$addon_duration}min)");
        }

        // Debug log
        yatoon_safe_log("SBS Availability Debug - service: {$service->name} (id={$service_id}), date={$date}, staff_id={$staff_id}");
        yatoon_safe_log("SBS Availability Debug - service square_id (ITEM): {$service->square_id}");
        yatoon_safe_log("SBS Availability Debug - selected_option_ids: " . json_encode($selected_option_ids));
        yatoon_safe_log("SBS Availability Debug - variations found: " . count($all_variations));
        foreach ($all_variations as $v) {
            yatoon_safe_log("  variation: id={$v->id}, name={$v->name}, square_id={$v->square_id}");
        }
        yatoon_safe_log("SBS Availability Debug - using variation_id: " . ($variation_id ?: 'NONE'));

        // If no options exist, the service has no variations - cannot query Square availability
        if (empty($variation_id)) {
            // Square mode: no variation ID means we can't check Square - tell user to call
            wp_send_json_error(array(
                'message'         => __('Unable to check availability for this service. Please call us to book.', 'yatoon-booking-system'),
                'no_availability' => true,
            ));
        }

        $timezone   = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz_obj     = new DateTimeZone($timezone);
        $date_start = new DateTime($date . ' 00:00:00', $tz_obj);
        $date_end   = new DateTime($date . ' 23:59:59', $tz_obj);
        $start_date = $date_start->format('c');
        $end_date   = $date_end->format('c');

        // Build the list of Square team member IDs to query.
        // staff_id > 0  -> one specific staff member.
        // staff_id = 0  -> "Any staff": query every eligible staff member individually
        //                  and union their slots. Never pass null to Square - that
        //                  returns a union of ALL staff across ALL their shifts, which
        //                  massively over-reports availability.
        $staff_square_ids_to_query = array();
        $sq_to_local_id = array(); // square_id -> local DB id, used for cart-busy check
        if ($staff_id > 0) {
            $s = yatoon_db_get_row($wpdb->prepare("SELECT square_id FROM $table_staff WHERE id = %d", $staff_id));
            if ($s && !empty($s->square_id)) {
                $staff_square_ids_to_query[] = $s->square_id;
            }
        } else {
            // Any staff: ONLY query staff who are assigned to this service.
            // Do NOT fall back to all staff - that causes staff who can't do the
            // service (e.g. Lisa for Acrylic) to bleed their free slots into results.
            $table_ss_sq  = $wpdb->prefix . 'yatoon_staff_services';
            $table_stf_sq = $wpdb->prefix . 'yatoon_staff';
            $eligible = yatoon_db_get_results($wpdb->prepare("
                SELECT st.id, st.square_id, st.name
                FROM $table_stf_sq st
                INNER JOIN $table_ss_sq ss ON st.id = ss.staff_id
                WHERE st.is_active = 1 AND st.is_bookable = 1 AND ss.service_id = %d
                ORDER BY st.sort_order ASC, st.name ASC
            ", $service_id));
            if (!empty($eligible)) {
                foreach ($eligible as $e) {
                    if (!empty($e->square_id)) $staff_square_ids_to_query[] = $e->square_id;
                }
            }
            yatoon_safe_log("SBS Availability Debug - Any staff for service_id=$service_id: found " . count($staff_square_ids_to_query) . " eligible staff in yatoon_staff_services");
            // Build square_id -> local_id map for cart-busy check
            $sq_to_local_id = array();
            foreach ($eligible as $e) {
                if (!empty($e->square_id)) $sq_to_local_id[$e->square_id] = intval($e->id);
            }

            // IMPORTANT: If yatoon_staff_services is empty for this service, we cannot safely
            // show availability - doing a fallback to ALL staff would include staff who
            // cannot perform this service, showing their free slots incorrectly.
            // Instead, return an error prompting the admin to rebuild service assignments.
            if (empty($staff_square_ids_to_query)) {
                wp_send_json_error(array(
                    'message'         => __('Staff-service assignments not configured. Please go to WP Admin -> Square Booking -> Staff and click "Rebuild Service Assignments" to fix availability.', 'yatoon-booking-system'),
                    'no_availability' => true,
                ));
            }
        }

        if (empty($staff_square_ids_to_query)) {
            wp_send_json_error(array(
                'message'         => __('No available staff for this service.', 'yatoon-booking-system'),
                'no_availability' => true,
            ));
        }

        yatoon_safe_log("SBS Availability Debug - start: $start_date  end: $end_date");

        // -- REVISED ANY-STAFF SLOT LOGIC -------------------------------------
        // For "Any staff" (staff_id = 0), a time slot is only available if AT LEAST
        // ONE eligible staff member is FREE at that time (not busy in the cart).
        //
        // Previous bug: $seen_times marked a slot as "done" even when skipped due to
        // cart-busy. So if Staff A's 3:30 was skipped (busy), Staff B's 3:30 still
        // passed through - even though in some cases only Staff A was eligible.
        //
        // Fix: collect per-slot data for ALL staff first, then decide per slot:
        //   - slot_free_staff[time]  = count of staff who are free at that time
        //   - slot_data[time]        = the start_at value for that slot
        // A slot is included in results only if slot_free_staff[time] >= 1.
        // ---------------------------------------------------------------------
        $slot_dur          = !empty($all_variations) ? intval($all_variations[0]->duration_minutes) : 60;
        $slot_free_staff    = array(); // time_key => count of free staff at that time
        $slot_start_at      = array(); // time_key => ISO start_at string
        $slot_free_staff_ids = array(); // time_key => [ square_staff_id, ... ]
        $variations_to_try = !empty($all_variations) ? $all_variations : array();

        // For named staff (staff_id > 0), build local_id map too so cart-busy works
        if ($staff_id > 0 && empty($sq_to_local_id) && !empty($staff_square_ids_to_query)) {
            $sq_id = $staff_square_ids_to_query[0];
            $row   = yatoon_db_get_row($wpdb->prepare("SELECT id FROM $table_staff WHERE square_id = %s LIMIT 1", $sq_id));
            if ($row) $sq_to_local_id[$sq_id] = intval($row->id);
        }

        // Helper: find a fallback bookable variation with similar duration
        $find_bookable_fallback = function($target_duration_min) use ($wpdb) {
            $table_opts = $wpdb->prefix . 'yatoon_service_options';
            // Get variations with close duration (within 30 min), ordered by closest match
            $rows = yatoon_db_get_results($wpdb->prepare(
                "SELECT square_id, duration_minutes FROM {$table_opts}
                 WHERE square_id != '' AND duration_minutes BETWEEN %d AND %d
                 ORDER BY ABS(duration_minutes - %d) ASC LIMIT 10",
                max(0, $target_duration_min - 30), $target_duration_min + 30, $target_duration_min
            ));
            return $rows ?: array();
        };

        foreach ($staff_square_ids_to_query as $sq_staff_id) {
            $local_id = isset($sq_to_local_id[$sq_staff_id]) ? $sq_to_local_id[$sq_staff_id] : 0;
            foreach ($variations_to_try as $var) {

                // -- If Add-on option IDs were sent, resolve their Square variation IDs
                // and use multi-segment availability query so Square validates the full
                // combined duration (main + addon) fits in a single continuous window.
                if (!empty($addon_option_ids)) {
                    $table_opts_av = $wpdb->prefix . 'yatoon_service_options';
                    $addon_ph      = implode(',', array_fill(0, count($addon_option_ids), '%d'));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $addon_rows    = yatoon_db_get_results(
                        $wpdb->prepare(
                            "SELECT square_id, duration_minutes FROM $table_opts_av WHERE id IN ($addon_ph) AND square_id != ''",
                            ...$addon_option_ids
                        )
                    );
                    if (!empty($addon_rows)) {
                        // Build multi-segment: [main, addon1, addon2, ...]
                        $multi_segments = array(array(
                            'variation_id'   => $var->square_id,
                            'team_member_id' => $sq_staff_id,
                        ));
                        foreach ($addon_rows as $addon_row) {
                            $multi_segments[] = array(
                                'variation_id'   => $addon_row->square_id,
                                'team_member_id' => $sq_staff_id,
                            );
                        }
                        $response = $this->square_api->search_availability_multi_segment($multi_segments, $start_date, $end_date);
                        yatoon_safe_log("SBS Availability Debug - staff={$sq_staff_id} MULTI-SEGMENT (main+addon) query: " . (is_wp_error($response) ? 'WP_ERROR: '.$response->get_error_message() : (isset($response['availabilities']) ? count($response['availabilities']).' slots' : 'no availabilities key')));
                    } else {
                        // Addon has no square_id - fall back to single-segment + local filter
                        $response = $this->square_api->search_availability($var->square_id, $start_date, $end_date, $sq_staff_id);
                        yatoon_safe_log("SBS Availability Debug - staff={$sq_staff_id} variation='{$var->name}' (addon has no sq_id, single-segment): " . (is_wp_error($response) ? 'WP_ERROR: '.$response->get_error_message() : (isset($response['availabilities']) ? count($response['availabilities']).' slots' : 'no availabilities key')));
                    }
                } else {
                    $response = $this->square_api->search_availability($var->square_id, $start_date, $end_date, $sq_staff_id);
                    yatoon_safe_log("SBS Availability Debug - staff={$sq_staff_id} variation='{$var->name}': " . (is_wp_error($response) ? 'WP_ERROR: '.$response->get_error_message() : (isset($response['availabilities']) ? count($response['availabilities']).' slots' : 'no availabilities key')));
                }

                // If this variation is not bookable on Square, try other variations with same duration
                if (!is_wp_error($response) && isset($response['errors'])) {
                    $is_not_bookable = false;
                    foreach ($response['errors'] as $err) {
                        if (!empty($err['detail']) && stripos($err['detail'], 'not bookable') !== false) {
                            $is_not_bookable = true;
                            break;
                        }
                    }
                    if ($is_not_bookable) {
                        yatoon_safe_log("SBS Availability - variation '{$var->name}' not bookable, trying fallback variations");
                        $fallbacks = $find_bookable_fallback(intval($var->duration_minutes));
                        foreach ($fallbacks as $fb) {
                            if ($fb->square_id === $var->square_id) continue;
                            $fb_resp = $this->square_api->search_availability($fb->square_id, $start_date, $end_date, $sq_staff_id);
                            if (!is_wp_error($fb_resp) && empty($fb_resp['errors']) && !empty($fb_resp['availabilities'])) {
                                yatoon_safe_log("SBS Availability - fallback variation {$fb->square_id} ({$fb->duration_minutes}min) returned " . count($fb_resp['availabilities']) . " slots");
                                $response = $fb_resp;
                                break;
                            }
                        }
                    }
                }

                if (is_wp_error($response)) continue;

                if (isset($response['availabilities']) && is_array($response['availabilities'])) {
                    foreach ($response['availabilities'] as $availability) {
                        if (empty($availability['start_at'])) continue;
                        $t = new DateTime($availability['start_at']);
                        $t->setTimezone(new DateTimeZone($timezone));
                        $slot_key = $t->format('H:i');

                        // Record the start_at for this slot (first time we see it)
                        if (!isset($slot_start_at[$slot_key])) {
                            $slot_start_at[$slot_key] = $availability['start_at'];
                            $slot_free_staff[$slot_key] = 0;
                            $slot_free_staff_ids[$slot_key] = array();
                        }

                        // Check if THIS staff member is free at this slot
                        if ($local_id > 0 && $is_staff_cart_busy($local_id, $t->getTimestamp(), $slot_dur)) {
                            // This staff is busy in the cart at this time - do NOT count them
                            continue;
                        }
                        // This staff is free - increment free-staff counter and record their Square ID
                        $slot_free_staff[$slot_key] = ($slot_free_staff[$slot_key] ?? 0) + 1;
                        $slot_free_staff_ids[$slot_key][] = $sq_staff_id;
                    }
                    if (!empty($response['availabilities'])) {
                        break; // got slots from this variation for this staff - try next variation
                    }
                }
            }
        } // end foreach staff

        // Build final available_times: only include slots where at least 1 staff is free
        $available_times = array();
        foreach ($slot_start_at as $slot_key => $start_at) {
            if (($slot_free_staff[$slot_key] ?? 0) >= 1) {
                $t = new DateTime($start_at);
                $t->setTimezone(new DateTimeZone($timezone));
                $available_times[] = array(
                    'time'          => $slot_key,
                    'display'       => $t->format('g:i A'),
                    'start_at'      => $start_at,
                    'free_staff_ids' => $slot_free_staff_ids[$slot_key] ?? array(),
                );
            }
        }

        // Sort by time
        usort($available_times, function($a, $b) { return strcmp($a['time'], $b['time']); });

        // -- Secondary filter: full-duration conflict check using Square live data --
        // Square's availability API only validates the MAIN variation duration.
        // When add-ons are selected (e.g. Custom Design = 30 min extra), a slot
        // returned by Square as "free for 45 min" may actually be blocked at
        // 45+30=75 min because the next booking starts within that window.
        //
        // IMPORTANT: Filter per-staff. A slot is kept if at least one free staff
        // member has no conflict in the full window. Don't use global busy_windows
        // that would incorrectly block slots for other staff.
        if ($addon_duration > 0 && !empty($available_times)) {
            $timezone_filter  = ( wp_timezone_string() ?: 'America/Chicago' );
            $tz_filter        = new DateTimeZone($timezone_filter);
            $total_duration   = 0;
            if (!empty($all_variations)) {
                $total_duration = intval($all_variations[0]->duration_minutes);
            }
            if (!$total_duration) $total_duration = intval($service->duration_minutes ?? 60);
            $total_duration += $addon_duration;

            // Fetch Square's live bookings for this day
            $sq_day_start = (new DateTime($date . ' 00:00:00', $tz_filter))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z');
            $sq_day_end   = (new DateTime($date . ' 23:59:59', $tz_filter))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z');
            $sq_live_bookings = $this->square_api->list_all_bookings($sq_day_start, $sq_day_end);

            // Build busy windows PER STAFF: [ staff_square_id => [ [start_ts, end_ts], ... ] ]
            $busy_by_staff = array();
            if (!empty($sq_live_bookings) && is_array($sq_live_bookings)) {
                foreach ($sq_live_bookings as $sq_bk) {
                    if (empty($sq_bk['start_at'])) continue;
                    $status = strtoupper($sq_bk['status'] ?? '');
                    if (in_array($status, array('CANCELLED', 'NO_SHOW', 'CANCELLED_BY_SELLER', 'CANCELLED_BY_CUSTOMER'), true)) continue;

                    // Get staff IDs for this booking
                    $bk_staff_ids = array();
                    if (!empty($sq_bk['appointment_segments'])) {
                        foreach ($sq_bk['appointment_segments'] as $seg) {
                            if (!empty($seg['team_member_id'])) $bk_staff_ids[] = $seg['team_member_id'];
                        }
                    }

                    // Calculate booking duration
                    $bk_dur = 0;
                    if (!empty($sq_bk['appointment_segments'])) {
                        foreach ($sq_bk['appointment_segments'] as $seg) {
                            if (!empty($seg['duration_minutes'])) $bk_dur += intval($seg['duration_minutes']);
                        }
                    }
                    if ($bk_dur <= 0) $bk_dur = 60;

                    $bk_start_ts = (new DateTime($sq_bk['start_at']))->getTimestamp();
                    $bk_end_ts   = $bk_start_ts + ($bk_dur * 60);

                    // Add to each involved staff's busy list
                    foreach ($bk_staff_ids as $bk_sq_staff_id) {
                        if (!isset($busy_by_staff[$bk_sq_staff_id])) $busy_by_staff[$bk_sq_staff_id] = array();
                        $busy_by_staff[$bk_sq_staff_id][] = array($bk_start_ts, $bk_end_ts);
                    }
                }
            }

            yatoon_safe_log("SBS Addon filter - total={$total_duration}min, staff_with_busy=" . count($busy_by_staff) . ", slots_before=" . count($available_times));

            // Filter slots: keep slot if at least one free staff has no conflict
            $filtered_times = array();
            foreach ($available_times as $slot) {
                $slot_start_ts  = (new DateTime($slot['start_at']))->getTimestamp();
                $slot_end_ts    = $slot_start_ts + ($total_duration * 60);
                $free_staff_ids = $slot['free_staff_ids'] ?? array();

                $has_free_staff = false;
                foreach ($free_staff_ids as $sq_staff_id) {
                    $staff_busy = $busy_by_staff[$sq_staff_id] ?? array();
                    $conflict = false;
                    foreach ($staff_busy as $bw) {
                        if ($slot_start_ts < $bw[1] && $slot_end_ts > $bw[0]) {
                            $conflict = true;
                            break;
                        }
                    }
                    if (!$conflict) {
                        $has_free_staff = true;
                        break;
                    }
                }

                if ($has_free_staff) $filtered_times[] = $slot;
            }

            yatoon_safe_log("SBS Addon filter - slots_after=" . count($filtered_times));
            $available_times = $filtered_times;
        }

        // Square returned no availability for this date. Keep Square as the
        // source of available slots, but improve the closed-day message.
        if (empty($available_times)) {
            wp_send_json_error(array(
                'message'         => ($external_closed_day_message() ?: __('No available times on this date.', 'yatoon-booking-system')),
                'no_availability' => true,
            ));
        }

        // Apply latest-end-time cutoff - use total_duration if hint was sent,
        // otherwise fall back to the $total_duration already computed for addon filter
        $cutoff_dur = $total_duration_hint > 0 ? $total_duration_hint
                    : (isset($total_duration) ? $total_duration : (isset($main_duration) ? $main_duration : 0));
        $available_times = $apply_cutoff($available_times, $cutoff_dur, $date);
        if (empty($available_times)) {
            wp_send_json_error(array('message' => __('No available times - all remaining slots would end after our closing time.', 'yatoon-booking-system'), 'no_availability' => true));
        }

        wp_send_json_success(array(
            'times' => $available_times,
            'count' => count($available_times),
            'mode'  => 'square',
        ));
    }

        /**
     * Generate default time slots
     */
    private function generate_default_times($date) {
        $times = array();
        $start_hour = 9; // 9 AM
        $end_hour = 18; // 6 PM
        $interval = 30; // 30-minute interval
        
        for ($hour = $start_hour; $hour < $end_hour; $hour++) {
            for ($minute = 0; $minute < 60; $minute += $interval) {
                $time = sprintf('%02d:%02d', $hour, $minute);
                $timestamp = strtotime($date . ' ' . $time);
                
                $times[] = array(
                    'time' => $time,
                    'display' => date('g:i A', $timestamp),
                    'start_at' => date('Y-m-d\TH:i:s\Z', $timestamp)
                );
            }
        }
        
        return $times;
    }
    
    /**
     * Find next available date (searches forward up to 60 days)
     * Used by the "Go to next available" button
     */
    public function get_next_available() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');

        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id'])        : 0;
        $staff_id   = isset($_POST['staff_id'])   ? intval($_POST['staff_id'])          : 0;
        $from_date  = isset($_POST['from_date'])  ? sanitize_text_field( wp_unslash( $_POST['from_date'] ) ) : '';

        if (!$service_id || !$from_date) {
            wp_send_json_error(array('message' => 'Missing parameters'));
        }

        $timezone_str = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz           = new DateTimeZone($timezone_str);
        $platform     = get_option('yatoon_active_platform', 'local');

        // Start searching from tomorrow
        $search_from = new DateTime($from_date, $tz);
        $search_from->modify('+1 day');
        $search_to   = (clone $search_from)->modify('+59 days'); // 60-day window

        global $wpdb;

        // -- SQUARE MODE ----------------------------------------------------------
        if ($platform === 'square') {

            $table_opts_na = $wpdb->prefix . 'yatoon_service_options';
            $table_stf_na  = $wpdb->prefix . 'yatoon_staff';

            // Helper: build correct date chunks (Square max window = 32 days)
            // Returns array of ['start' => ISO8601, 'end' => ISO8601]
            $build_chunks = function($from_dt, $num_chunks = 2) {
                $chunks = array();
                for ($i = 0; $i < $num_chunks; $i++) {
                    $s = clone $from_dt;
                    $s->modify('+' . ($i * 30) . ' days')->setTime(0, 0, 0);
                    $e = clone $s;
                    $e->modify('+29 days')->setTime(23, 59, 59);
                    $chunks[] = array('start' => $s->format('c'), 'end' => $e->format('c'));
                }
                return $chunks;
            };

            // Helper: resolve Square variation IDs from option DB IDs (or fallback to service default)
            // KEY FIX: If user selected specific options, use ONLY those variations.
            // Never add other variations as fallback - they have different durations
            // and would cause Square to return availability for the wrong service length.
            $resolve_vars = function($opt_ids, $svc_id) use ($wpdb, $table_opts_na) {
                $vars = array();
                if (!empty($opt_ids)) {
                    $clean = array_map('intval', (array)$opt_ids);
                    $ph    = implode(',', array_fill(0, count($clean), '%d'));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $rows  = yatoon_db_get_results(
                        $wpdb->prepare("SELECT square_id FROM $table_opts_na WHERE id IN ($ph) AND square_id != ''", ...$clean)
                    );
                    foreach ($rows as $r) $vars[] = $r->square_id;
                    // Do NOT add fallback here - user chose a specific option with a specific duration
                }
                // Only use default when no option was selected at all
                if (empty($vars) && $svc_id) {
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $rows = yatoon_db_get_results(
                        $wpdb->prepare("SELECT square_id FROM $table_opts_na WHERE service_id = %d AND square_id != '' ORDER BY id ASC LIMIT 1", $svc_id)
                    );
                    foreach ($rows as $r) $vars[] = $r->square_id;
                }
                return $vars;
            };

            // Helper: search all chunks for earliest date using single-service query
            $find_earliest_single = function($variation_ids, $sq_staff_id) use ($build_chunks, $search_from, $tz) {
                $chunks   = $build_chunks($search_from, 2);
                $earliest = null;
                $disp     = null;
                foreach ($chunks as $chunk) {
                    if ($earliest !== null) break;
                    foreach ($variation_ids as $var_id) {
                        $resp = $this->square_api->search_availability($var_id, $chunk['start'], $chunk['end'], $sq_staff_id);
                        if (is_wp_error($resp) || empty($resp['availabilities'])) continue;
                        foreach ($resp['availabilities'] as $av) {
                            if (empty($av['start_at'])) continue;
                            $t = new DateTime($av['start_at']);
                            $t->setTimezone($tz);
                            $d = $t->format('Y-m-d');
                            if ($earliest === null || $d < $earliest) {
                                $earliest = $d;
                                $disp     = $t->format('l, F j');
                            }
                        }
                        break; // first working variation is enough
                    }
                }
                return array($earliest, $disp);
            };

            // Helper: search all chunks for earliest date using multi-segment query
            $find_earliest_multi = function($segments) use ($build_chunks, $search_from, $tz) {
                $chunks   = $build_chunks($search_from, 2);
                $earliest = null;
                $disp     = null;
                foreach ($chunks as $chunk) {
                    if ($earliest !== null) break;
                    $resp = $this->square_api->search_availability_multi_segment($segments, $chunk['start'], $chunk['end']);
                    if (is_wp_error($resp) || empty($resp['availabilities'])) continue;
                    foreach ($resp['availabilities'] as $av) {
                        if (empty($av['start_at'])) continue;
                        $t = new DateTime($av['start_at']);
                        $t->setTimezone($tz);
                        $d = $t->format('Y-m-d');
                        if ($earliest === null || $d < $earliest) {
                            $earliest = $d;
                            $disp     = $t->format('l, F j');
                        }
                    }
                }
                return array($earliest, $disp);
            };

            // -- Parse multi-service params (sent from frontend for multi-service bookings) --
            $ms_staff_ids  = array();
            $ms_option_ids = array();
            $ms_guest_labels = array();
            $raw_sids = isset($_POST['all_staff_ids'])  ? stripslashes($_POST['all_staff_ids'])  : '';
            $raw_oids = isset($_POST['all_option_ids']) ? stripslashes($_POST['all_option_ids']) : '';
            $raw_guest_labels = isset($_POST['all_guest_labels']) ? stripslashes($_POST['all_guest_labels']) : '';
            if (!empty($raw_sids)) {
                $d = json_decode($raw_sids, true);
                if (is_array($d)) $ms_staff_ids = array_map('intval', $d);
            }
            if (!empty($raw_oids)) {
                $d = json_decode($raw_oids, true);
                if (is_array($d)) $ms_option_ids = $d;
            }
            if (!empty($raw_guest_labels)) {
                $d = json_decode($raw_guest_labels, true);
                if (is_array($d)) $ms_guest_labels = array_map('sanitize_text_field', $d);
            }

            // -- PATH A: Multi-service booking (2+ services in pendingServices) -------------
            if (count($ms_staff_ids) >= 2) {
                $has_any = in_array(0, $ms_staff_ids, true);

                // Helper: get service_id for a pending position (from option's service, fallback to $service_id)
                $get_svc_for_pos = function($opt_ids) use ($wpdb, $service_id) {
                    if (!empty($opt_ids)) {
                        $first_opt = intval($opt_ids[0]);
                        if ($first_opt) {
                            $row = yatoon_db_get_var($wpdb->prepare(
                                "SELECT service_id FROM {$wpdb->prefix}yatoon_service_options WHERE id = %d LIMIT 1",
                                $first_opt
                            ));
                            if ($row) return intval($row);
                        }
                    }
                    return $service_id;
                };

                if (!$has_any) {
                    // CASE A: All named staff - build one multi-segment query per guest.
                    // Same guest services remain sequential; different guests start together.
                    $guest_segs = array(); // guest_label => [segment, ...]
                    $guest_offsets = array();
                    $staff_windows = array();
                    foreach ($ms_staff_ids as $idx => $s_id) {
                        $s_row = yatoon_db_get_row($wpdb->prepare("SELECT square_id FROM $table_stf_na WHERE id = %d", $s_id));
                        if (!$s_row || empty($s_row->square_id)) continue;
                        $opts   = isset($ms_option_ids[$idx]) ? (array)$ms_option_ids[$idx] : array();
                        $svc_id = $get_svc_for_pos($opts);
                        $vars   = $resolve_vars($opts, $svc_id);
                        if (empty($vars)) continue;
                        $sq_sid = $s_row->square_id;
                        $guest_label = isset($ms_guest_labels[$idx]) && $ms_guest_labels[$idx] !== '' ? $ms_guest_labels[$idx] : 'Me';
                        if (!isset($guest_offsets[$guest_label])) $guest_offsets[$guest_label] = 0;

                        $dur = 60;
                        if (!empty($opts)) {
                            $d = yatoon_db_get_var($wpdb->prepare(
                                "SELECT duration_minutes FROM {$wpdb->prefix}yatoon_service_options WHERE id = %d LIMIT 1",
                                intval($opts[0])
                            ));
                            if ($d) $dur = intval($d);
                        }
                        $offset = intval($guest_offsets[$guest_label]);
                        $guest_offsets[$guest_label] += $dur;

                        if (!isset($staff_windows[$sq_sid])) $staff_windows[$sq_sid] = array();
                        foreach ($staff_windows[$sq_sid] as $w) {
                            if ($offset < $w['end'] && ($offset + $dur) > $w['start']) {
                                wp_send_json_error(array('message' => 'No availability found in the next 60 days.'));
                            }
                        }
                        $staff_windows[$sq_sid][] = array('start' => $offset, 'end' => $offset + $dur);

                        if (!isset($guest_segs[$guest_label])) $guest_segs[$guest_label] = array();
                        $guest_segs[$guest_label][] = array('variation_id' => $vars[0], 'team_member_id' => $sq_sid);
                    }

                    if (!empty($guest_segs)) {
                        // Find the date where ALL guests have availability to begin together.
                        $per_guest_earliest = array();
                        foreach ($guest_segs as $guest_label => $segs) {
                            list($d, $dp) = $find_earliest_multi($segs);
                            $per_guest_earliest[$guest_label] = $d;
                        }
                        // All guests must be free on the same day, so use the latest
                        // individual earliest date as the first date worth showing.
                        $latest_date = null;
                        $latest_disp = null;
                        foreach ($per_guest_earliest as $guest_label => $d) {
                            if ($d === null) { $latest_date = null; break; } // one staff has no avail
                            if ($latest_date === null || $d > $latest_date) {
                                $latest_date = $d;
                                $t = new DateTime($d . ' 00:00:00', $tz);
                                $latest_disp = $t->format('l, F j');
                            }
                        }
                        if ($latest_date) {
                            wp_send_json_success(array('date' => $latest_date, 'display' => $latest_disp));
                        }
                        wp_send_json_error(array('message' => 'No availability found in the next 60 days.'));
                    }
                } else {
                    // CASE B: At least one "Any staff" - collect all any-service variations,
                    // then query each bookable staff member with ALL combined variations.
                    $all_vars_for_any = array();
                    foreach ($ms_staff_ids as $idx => $s_id) {
                        $opts   = isset($ms_option_ids[$idx]) ? (array)$ms_option_ids[$idx] : array();
                        $svc_id = $get_svc_for_pos($opts);
                        $v_list = $resolve_vars($opts, $svc_id);
                        if (!empty($v_list)) $all_vars_for_any[] = $v_list[0];
                    }

                    if (!empty($all_vars_for_any)) {
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                        $all_candidates = yatoon_db_get_results(
                            "SELECT square_id, name FROM $table_stf_na WHERE is_active=1 AND is_bookable=1 AND square_id != '' ORDER BY sort_order ASC, name ASC"
                        );

                        $earliest_date = null;
                        $earliest_disp = null;

                        foreach ($all_candidates as $cand) {
                            // Build combined multi-segment: all services done by this one staff
                            $segs = array();
                            foreach ($all_vars_for_any as $vid) {
                                $segs[] = array('variation_id' => $vid, 'team_member_id' => $cand->square_id);
                            }
                            list($d, $dp) = $find_earliest_multi($segs);
                            if ($d !== null && ($earliest_date === null || $d < $earliest_date)) {
                                $earliest_date = $d;
                                $earliest_disp = $dp;
                            }
                        }

                        if ($earliest_date) {
                            wp_send_json_success(array('date' => $earliest_date, 'display' => $earliest_disp));
                        }
                        wp_send_json_error(array('message' => 'No availability found in the next 60 days.'));
                    }
                }
                // fall through to single-service logic if we couldn't resolve variations
            }

            // -- PATH B: Single-service booking ---------------------------------------------
            $staff_square_id = null;
            if ($staff_id) {
                $s_row = yatoon_db_get_row($wpdb->prepare("SELECT square_id FROM $table_stf_na WHERE id = %d", $staff_id));
                if ($s_row) $staff_square_id = $s_row->square_id;
            }

            // Collect variation IDs to try (selected options first, then all variations as fallback)
            $sel_opt_ids = array();
            if (!empty($_POST['option_ids'])) {
                $raw = json_decode(stripslashes($_POST['option_ids']), true);
                if (is_array($raw)) $sel_opt_ids = array_map('intval', $raw);
            }

            $all_variation_ids = array();
            if (!empty($sel_opt_ids)) {
                $ph   = implode(',', array_fill(0, count($sel_opt_ids), '%d'));
                $rows = yatoon_db_get_results($wpdb->prepare("SELECT square_id FROM $table_opts_na WHERE id IN ($ph) AND square_id != ''", ...$sel_opt_ids));
                foreach ($rows as $r) $all_variation_ids[] = $r->square_id;
            }
            // Always add all service variations as fallback
            $rows = yatoon_db_get_results($wpdb->prepare("SELECT square_id FROM $table_opts_na WHERE service_id = %d AND square_id != '' ORDER BY id ASC", $service_id));
            foreach ($rows as $r) {
                if (!in_array($r->square_id, $all_variation_ids)) $all_variation_ids[] = $r->square_id;
            }

            if (empty($all_variation_ids)) {
                wp_send_json_error(array('message' => 'No availability found. Please call us to book.'));
            }

            list($earliest_date, $earliest_disp) = $find_earliest_single($all_variation_ids, $staff_square_id);

            if (!$earliest_date) {
                wp_send_json_error(array('message' => 'No availability found in the next 60 days.'));
            }

            wp_send_json_success(array('date' => $earliest_date, 'display' => $earliest_disp));
        }

        // -- VAGARO MODE: try day-by-day (Vagaro API doesn't support multi-day range) --
        if ($platform === 'vagaro') {
            if (!class_exists('YATOON_Vagaro_API')) {
                require_once SBS_PLUGIN_DIR . 'includes/class-yatoon-vagaro-api.php';
            }
            $vagaro = new YATOON_Vagaro_API();
            $svc    = yatoon_db_get_row($wpdb->prepare(
                "SELECT square_id FROM {$wpdb->prefix}yatoon_services WHERE id = %d", $service_id
            ));
            $vagaro_service_id  = $svc ? $svc->square_id : null;
            $vagaro_employee_id = null;
            if ($staff_id) {
                $stf = yatoon_db_get_row($wpdb->prepare(
                    "SELECT square_id FROM {$wpdb->prefix}yatoon_staff WHERE id = %d", $staff_id
                ));
                if ($stf) $vagaro_employee_id = $stf->square_id;
            }

            if (!$vagaro_service_id) {
                wp_send_json_error(array('message' => 'No availability found. Please call us to book.'));
            }

            $cursor   = clone $search_from;
            $max_days = 60;
            for ($i = 0; $i < $max_days; $i++) {
                $check_date = $cursor->format('Y-m-d');
                $resp = $vagaro->get_availability($vagaro_service_id, $vagaro_employee_id, $check_date);
                if (!is_wp_error($resp)) {
                    $slots_raw = $resp['slots'] ?? $resp['availabilities'] ?? $resp['times'] ?? array();
                    if (!empty($slots_raw)) {
                        wp_send_json_success(array(
                            'date'    => $check_date,
                            'display' => $cursor->format('l, F j'),
                        ));
                    }
                }
                $cursor->modify('+1 day');
            }
            wp_send_json_error(array('message' => 'No availability found in the next 60 days.'));
        }

        // -- LOCAL MODE: fast - just check business hours + existing bookings --
        $cursor   = clone $search_from;
        $max_days = 60;
        for ($i = 0; $i < $max_days; $i++) {
            $check_date = $cursor->format('Y-m-d');
            $day_hours  = YATOON_Business_Hours::get_day_hours($check_date);
            if ($day_hours) {
                $slots = YATOON_Business_Hours::generate_slots($check_date, $service_id, $staff_id);
                if (!empty($slots)) {
                    wp_send_json_success(array(
                        'date'    => $check_date,
                        'display' => $cursor->format('l, F j'),
                    ));
                }
            }
            $cursor->modify('+1 day');
        }
        wp_send_json_error(array('message' => 'No availability found in the next 60 days.'));
    }

    /**
     * Acquire a short-lived MySQL named lock around local booking creation.
     *
     * This closes the race where two customers submit the same visible local
     * slot at nearly the same moment. Square/Vagaro remain the source of truth
     * for their own modes, so this lock is only used before local DB inserts.
     */
    private function acquire_local_booking_lock($start_at, $service_ids, $timeout = 8) {
        global $wpdb;

        $service_ids = array_values(array_unique(array_filter(array_map('intval', (array) $service_ids))));
        sort($service_ids);

        $bucket = '';
        try {
            $dt = new DateTime($start_at, wp_timezone());
            $bucket = $dt->format('Y-m-d H:i');
        } catch (Exception $e) {
            $bucket = sanitize_text_field((string) $start_at);
        }

        $raw_key = home_url() . '|' . $bucket . '|' . implode(',', $service_ids);
        $lock_key = 'yatoon_booking_' . md5($raw_key);
        $got_lock = (int) yatoon_db_get_var($wpdb->prepare('SELECT GET_LOCK(%s, %d)', $lock_key, max(1, (int) $timeout)));

        return $got_lock === 1 ? $lock_key : false;
    }

    private function release_local_booking_lock($lock_key) {
        if (!$lock_key) {
            return;
        }

        global $wpdb;
        yatoon_db_get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_key));
    }

    private function resolve_design_reference_upload() {
        $attachment_id = isset($_POST['design_attachment_id']) ? absint($_POST['design_attachment_id']) : 0;
        if ($attachment_id > 0) {
            $url = wp_get_attachment_url($attachment_id);
            if ($url && wp_attachment_is_image($attachment_id)) {
                $stored_token = (string) get_post_meta($attachment_id, '_yatoon_design_upload_token', true);
                $provided_token = isset($_POST['design_upload_token']) ? sanitize_text_field(wp_unslash($_POST['design_upload_token'])) : '';
                if ($stored_token !== '' && ($provided_token === '' || !hash_equals($stored_token, $provided_token))) {
                    wp_send_json_error(array('message' => __('The design image authorization expired. Please choose the image again.', 'yatoon-booking-system')));
                }
                return array(
                    'design_attachment_id' => $attachment_id,
                    'design_image_url'     => esc_url_raw($url),
                    'design_upload_token'  => $stored_token,
                );
            }
        }

        if (empty($_FILES['design_reference']) || empty($_FILES['design_reference']['tmp_name'])) {
            return array(
                'design_attachment_id' => null,
                'design_image_url'     => null,
                'design_upload_token'  => '',
            );
        }

        $file = $_FILES['design_reference'];
        if (!empty($file['size']) && (int) $file['size'] > 10 * 1024 * 1024) {
            wp_send_json_error(array('message' => __('Please upload a design image smaller than 10 MB.', 'yatoon-booking-system')));
        }

        $allowed_mimes = array(
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png'          => 'image/png',
            'gif'          => 'image/gif',
            'webp'         => 'image/webp',
        );
        $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $allowed_mimes);
        if (empty($checked['type']) || strpos($checked['type'], 'image/') !== 0) {
            wp_send_json_error(array('message' => __('Please upload a JPG, PNG, GIF, or WebP design image.', 'yatoon-booking-system')));
        }

        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $upload = wp_handle_upload($file, array(
            'test_form' => false,
            'mimes'     => $allowed_mimes,
        ));
        if (!empty($upload['error'])) {
            wp_send_json_error(array('message' => sprintf(__('Design image upload failed: %s', 'yatoon-booking-system'), $upload['error'])));
        }

        $attachment = array(
            'post_mime_type' => $upload['type'],
            'post_title'     => sanitize_file_name(pathinfo($upload['file'], PATHINFO_FILENAME)),
            'post_content'   => '',
            'post_status'    => 'inherit',
        );
        $attachment_id = wp_insert_attachment($attachment, $upload['file']);
        if (is_wp_error($attachment_id) || !$attachment_id) {
            wp_send_json_error(array('message' => __('Design image could not be saved to the media library.', 'yatoon-booking-system')));
        }
        $upload_token = wp_generate_password(32, false, false);
        update_post_meta($attachment_id, '_yatoon_design_upload_token', $upload_token);
        update_post_meta($attachment_id, '_yatoon_design_pending_at', time());

        $design_sizes = static function($sizes) {
            return array_intersect_key((array) $sizes, array('thumbnail' => true, 'medium' => true));
        };
        add_filter('intermediate_image_sizes_advanced', $design_sizes, 999);
        $metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
        remove_filter('intermediate_image_sizes_advanced', $design_sizes, 999);
        if (!is_wp_error($metadata) && !empty($metadata)) {
            wp_update_attachment_metadata($attachment_id, $metadata);
        }

        return array(
            'design_attachment_id' => absint($attachment_id),
            'design_image_url'     => esc_url_raw($upload['url']),
            'design_upload_token'  => $upload_token,
        );
    }

    public function upload_design_reference() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');
        $this->enforce_public_rate_limit('design_upload', 8, 10);
        if (get_option('yatoon_allow_design_upload', '1') !== '1') {
            wp_send_json_error(array('message' => __('Design image uploads are currently disabled.', 'yatoon-booking-system')));
        }
        if (empty($_FILES['design_reference']['tmp_name'])) {
            wp_send_json_error(array('message' => __('Please choose an image first.', 'yatoon-booking-system')));
        }
        $result = $this->resolve_design_reference_upload();
        wp_send_json_success(array(
            'design_attachment_id' => absint($result['design_attachment_id'] ?? 0),
            'design_image_url' => esc_url_raw($result['design_image_url'] ?? ''),
            'design_upload_token' => sanitize_text_field($result['design_upload_token'] ?? ''),
            'message' => __('Photo ready.', 'yatoon-booking-system'),
        ));
    }

    public function finalize_design_reference($booking_data) {
        $attachment_id = absint($booking_data['design_attachment_id'] ?? 0);
        if ($attachment_id) {
            delete_post_meta($attachment_id, '_yatoon_design_pending_at');
        }
    }

    public function cleanup_design_uploads() {
        global $wpdb;
        $cutoff = time() - (2 * DAY_IN_SECONDS);
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        $ids = get_posts(array(
            'post_type' => 'attachment',
            'post_status' => 'inherit',
            'posts_per_page' => 50,
            'fields' => 'ids',
            'meta_query' => array(array(
                'key' => '_yatoon_design_pending_at',
                'value' => $cutoff,
                'compare' => '<',
                'type' => 'NUMERIC',
            )),
        ));
        foreach ($ids as $attachment_id) {
            $used = (int) yatoon_db_get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->prefix}yatoon_bookings WHERE design_attachment_id = %d",
                $attachment_id
            ));
            if (!$used) {
                wp_delete_attachment($attachment_id, true);
            } else {
                delete_post_meta($attachment_id, '_yatoon_design_pending_at');
            }
        }
    }

    /**
     * Create booking
     */
    public function create_booking() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');

        // -- Layer 0: Bot traps (silent, zero friction for real visitors) ----
        // Honeypot: a field hidden off-screen that only non-visual bots fill in.
        if (!empty($_POST['website'])) {
            wp_send_json_error(array('message' => __('Unable to complete this booking. Please call us directly.', 'yatoon-booking-system')));
        }
        // Timing trap: real visitors take at least a couple seconds to read
        // the form and pick a service/staff/time before typing their details.
        // Scripts that fetch the page and POST immediately get caught here.
        $rendered_at = isset($_POST['form_rendered_at']) ? intval($_POST['form_rendered_at']) : 0;
        if ($rendered_at > 0) {
            $elapsed = time() - $rendered_at;
            $min_seconds = intval(get_option('yatoon_min_submit_seconds', 3));
            if ($elapsed >= 0 && $elapsed < $min_seconds) {
                wp_send_json_error(array('message' => __('Unable to complete this booking. Please call us directly.', 'yatoon-booking-system')));
            }
        }

        // Cloudflare Turnstile (optional - only enforced if configured in Settings > Protection)
        // Token is fetched fresh at submit time (execute mode). If the widget
        // never returned a token (script blocked, network hiccup, ad blocker),
        // we fail OPEN rather than block a real customer's booking - the
        // other layers (honeypot, timing, blacklist) still apply either way.
        // We only ever REJECT here when a token was actually returned and
        // Cloudflare says it's invalid.
        if (get_option('yatoon_turnstile_enabled', '0') === '1' && !empty(get_option('yatoon_turnstile_secret_key'))) {
            $ts_token = isset($_POST['cf_turnstile_response']) ? sanitize_text_field(wp_unslash($_POST['cf_turnstile_response'])) : '';
            $turnstile_fail_closed = get_option('yatoon_turnstile_fail_closed', '0') === '1';
            if (empty($ts_token) && $turnstile_fail_closed) {
                wp_send_json_error(array('message' => __('We could not verify this request. Please refresh the page and try again.', 'yatoon-booking-system')));
            }
            if (!empty($ts_token) && !$this->verify_turnstile($ts_token)) {
                wp_send_json_error(array('message' => __('We could not verify this request. Please refresh the page and try again.', 'yatoon-booking-system')));
            }
        }

        // Extend execution time: Square API calls (search_availability x staff + create_booking)
        // can take 10-25 seconds total. Default PHP limit (30s) causes silent timeouts.
        @set_time_limit(120);
        global $wpdb;

        // Determine initial booking status based on Auto-Confirm setting
        $initial_status = get_option('yatoon_auto_confirm_bookings', '1') === '1' ? 'confirmed' : 'pending';
        $is_test_booking = get_option('yatoon_test_booking_mode', '0') === '1'
            && current_user_can('manage_options')
            && !empty($_POST['yatoon_test_booking']);
        if ($is_test_booking) {
            $initial_status = 'pending';
        }

        // Common input
	        $service_id      = isset($_POST['service_id'])      ? intval($_POST['service_id'])              : 0;
	        $staff_id        = isset($_POST['staff_id'])        ? intval($_POST['staff_id'])                : 0;
	        $requested_staff_id = $staff_id;
	        $start_at        = isset($_POST['start_at'])        ? sanitize_text_field( wp_unslash( $_POST['start_at'] ) )   : '';
        $customer_name   = isset($_POST['customer_name'])   ? sanitize_text_field( wp_unslash( $_POST['customer_name'] ) ) : '';
        $customer_email  = isset($_POST['customer_email'])  ? sanitize_email( wp_unslash( $_POST['customer_email'] ) )  : '';
        $customer_phone  = isset($_POST['customer_phone'])  ? sanitize_text_field( wp_unslash( $_POST['customer_phone'] ) ) : '';
        $otp_channel     = isset($_POST['otp_channel'])     ? sanitize_key( wp_unslash( $_POST['otp_channel'] ) ) : '';
        $otp_verified_key = isset($_POST['otp_verified_key']) ? sanitize_text_field( wp_unslash( $_POST['otp_verified_key'] ) ) : '';
        $notes           = isset($_POST['notes'])           ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) )  : '';
        if ($is_test_booking) {
            $notes = trim("[Yatoon test booking]\n" . $notes);
        }
        $tip_amount      = isset($_POST['tip_amount'])      ? max(0, floatval($_POST['tip_amount'])) : 0;
        $gift_card_code  = isset($_POST['gift_card_code'])  ? strtoupper(sanitize_text_field(wp_unslash($_POST['gift_card_code']))) : '';
        $gift_card_amount = 0;

        // Validate and reserve gift card if provided
        if ($gift_card_code) {
            global $wpdb;
            $gc_table = $wpdb->prefix . 'yatoon_gift_cards';
            $gc = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $gc_table WHERE code = %s AND status = 'active' LIMIT 1", $gift_card_code));
            if ($gc && floatval($gc->balance) > 0 && (!$gc->expires_at || strtotime($gc->expires_at) >= time())) {
                $gift_card_amount = floatval($gc->balance);
            }
        }

        $custom_fields_raw = isset($_POST['custom_fields']) ? sanitize_textarea_field(wp_unslash($_POST['custom_fields'])) : '{}';
        $custom_fields_decoded = json_decode($custom_fields_raw, true);
        $custom_field_values = is_array($custom_fields_decoded) ? wp_json_encode($custom_fields_decoded) : null;
        $guest_label     = isset($_POST['guest_label'])     ? sanitize_text_field( wp_unslash( $_POST['guest_label'] ) ) : '';
        $guest_name      = isset($_POST['guest_name'])      ? sanitize_text_field( wp_unslash( $_POST['guest_name'] ) ) : '';
        $guest_phone     = isset($_POST['guest_phone'])     ? sanitize_text_field( wp_unslash( $_POST['guest_phone'] ) ) : '';
        $booking_group_id = isset($_POST['booking_group_id']) ? substr( sanitize_key( wp_unslash( $_POST['booking_group_id'] ) ), 0, 36 ) : '';
        $coupon_code     = isset($_POST['coupon_code'])     ? strtoupper(sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) )) : '';
        $selected_options = isset($_POST['selected_options']) ? array_map('intval', (array)$_POST['selected_options']) : array();
        // The coupon is atomically claimed AFTER all validation passes, so a
        // failed-validation submission cannot increment the usage counter. See
        // claim_coupon() block further down (just before the platform branch).
        $claimed_coupon = null;
        $consent_enabled   = get_option('yatoon_consent_enabled', '0') === '1';
        $consent_accepted  = !empty($_POST['consent_accepted']) ? 1 : 0;
        $consent_signature = isset($_POST['consent_signature']) ? sanitize_text_field( wp_unslash( $_POST['consent_signature'] ) ) : '';
        $policy_enabled    = get_option('yatoon_policy_enabled', '0') === '1';
        $policy_require_ack = get_option('yatoon_policy_require_ack', '1') === '1';
        $policy_acknowledged = !empty($_POST['policy_acknowledged']) ? 1 : 0;

        // -- Stripe deposit verification (Pro only) ----------------------------
        $deposit_intent_id = isset($_POST['deposit_intent_id']) ? sanitize_text_field( wp_unslash( $_POST['deposit_intent_id'] ) ) : '';
        $square_payment_id = isset($_POST['square_payment_id']) ? sanitize_text_field( wp_unslash( $_POST['square_payment_id'] ) ) : '';
        $deposit_fields    = array();
        $square_payments_enabled = class_exists('Yatoon_Square_Payments') && Yatoon_Square_Payments::is_enabled();
        $deposit_enabled_opt = !$square_payments_enabled
                               && get_option('yatoon_deposit_enabled', '0') === '1'
                               && !empty(YATOON_Crypto::get_option('yatoon_deposit_stripe_secret', ''));
        if ( $square_payments_enabled ) {
            $booking_total = Yatoon_Square_Payments::calculate_total_from_request( $_POST );
            if ( is_wp_error( $booking_total ) ) {
                wp_send_json_error( array( 'message' => $booking_total->get_error_message() ) );
            }
            if ( $booking_total > 0 ) {
                if ( empty( $square_payment_id ) ) {
                    wp_send_json_error( array( 'message' => __('A Square payment is required to complete this booking.', 'yatoon-booking-system') ) );
                }
                $square_reference = 'square:' . $square_payment_id;
                $existing_square_payment_booking = yatoon_db_get_var(
                    $wpdb->prepare(
                        "SELECT id FROM {$wpdb->prefix}yatoon_bookings WHERE deposit_intent_id = %s LIMIT 1",
                        $square_reference
                    )
                );
                if ( $existing_square_payment_booking ) {
                    wp_send_json_error( array( 'message' => __('This Square payment has already been used for a booking.', 'yatoon-booking-system') ) );
                }
                $expected_cents = Yatoon_Square_Payments::calculate_payment_cents( $booking_total );
                $verify = Yatoon_Square_Payments::verify_payment( $square_payment_id, $expected_cents );
                if ( is_wp_error( $verify ) ) {
                    wp_send_json_error( array( 'message' => $verify->get_error_message() ) );
                }
                $deposit_amount = round( ( $verify['amount'] ?? 0 ) / 100, 2 );
                $deposit_fields = array(
                    'deposit_paid'      => 1,
                    'deposit_intent_id' => $square_reference,
                    'deposit_amount'    => $deposit_amount,
                    'payment_status'    => 'deposit_paid',
                );
            }
        }
        if ( $deposit_enabled_opt ) {
            // calculate_total_from_request() now factors in active package /
            // membership / coupon discounts, so $booking_total reflects the
            // actual amount due at the salon. If discounts cover the whole
            // booking, skip the deposit step entirely.
            $booking_total = Yatoon_Stripe::calculate_total_from_request( $_POST );
            if ( is_wp_error( $booking_total ) ) {
                wp_send_json_error( array( 'message' => $booking_total->get_error_message() ) );
            }
            if ( $booking_total > 0 ) {
                if ( empty( $deposit_intent_id ) ) {
                    wp_send_json_error( array( 'message' => __('A deposit payment is required to complete this booking.', 'yatoon-booking-system') ) );
                }
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $existing_deposit_booking = yatoon_db_get_var(
                    $wpdb->prepare(
                        "SELECT id FROM {$wpdb->prefix}yatoon_bookings WHERE deposit_intent_id = %s LIMIT 1",
                        $deposit_intent_id
                    )
                );
                if ( $existing_deposit_booking ) {
                    wp_send_json_error( array( 'message' => __('This deposit payment has already been used for a booking.', 'yatoon-booking-system') ) );
                }
                $verify = Yatoon_Stripe::verify_payment( $deposit_intent_id, Yatoon_Stripe::calculate_deposit_cents( $booking_total ) );
                if ( is_wp_error( $verify ) ) {
                    wp_send_json_error( array( 'message' => $verify->get_error_message() ) );
                }
                $deposit_amount = round( ( $verify['amount'] ?? 0 ) / 100, 2 );
                $deposit_fields = array(
                    'deposit_paid'      => 1,
                    'deposit_intent_id' => $deposit_intent_id,
                    'deposit_amount'    => $deposit_amount,
                    'payment_status'    => 'deposit_paid',
                );
            }
        }

        $booking_service_ids = array();
        if (!empty($_POST['services_json'])) {
            $posted_services = json_decode(wp_unslash($_POST['services_json']), true);
            if (is_array($posted_services)) {
                foreach ($posted_services as $posted_service) {
                    if (!empty($posted_service['service_id'])) {
                        $booking_service_ids[] = intval($posted_service['service_id']);
                    }
                }
            }
        }
        if (empty($booking_service_ids) && $service_id) {
            $booking_service_ids[] = $service_id;
        }
        $booking_service_ids = array_values(array_unique(array_filter($booking_service_ids)));

        $service_requires_policy = false;
        $service_requires_consent = false;
        if (!empty($booking_service_ids)) {
            $placeholders = implode(',', array_fill(0, count($booking_service_ids), '%d'));
            $service_rules = yatoon_db_get_results($wpdb->prepare(
                "SELECT id, require_policy, require_consent FROM {$wpdb->prefix}yatoon_services WHERE id IN ($placeholders)",
                $booking_service_ids
            ));
            foreach ((array) $service_rules as $service_rule) {
                if (!empty($service_rule->require_policy)) {
                    $service_requires_policy = true;
                }
                if (!empty($service_rule->require_consent)) {
                    $service_requires_consent = true;
                }
            }
        }
        $policy_ack_required = ($policy_enabled && $policy_require_ack) || $service_requires_policy;
        $policy_should_save = $policy_enabled || $service_requires_policy;
        $consent_required = $consent_enabled || $service_requires_consent;

        if (!$service_id || !$start_at || !$customer_name || !$customer_email || !$customer_phone) {
            wp_send_json_error(array('message' => __('Please fill in all required fields (including phone number)', 'yatoon-booking-system')));
        }
        $requires_email_otp = get_option('yatoon_verify_email_otp', '0') === '1';
        $requires_sms_otp   = get_option('yatoon_verify_sms_otp', '0') === '1';
        if ($requires_email_otp || $requires_sms_otp) {
            $required_channel = $requires_sms_otp ? 'sms' : 'email';
            $otp_channel = in_array($otp_channel, array('email', 'sms'), true) ? $otp_channel : $required_channel;
            if ($otp_channel !== $required_channel) {
                wp_send_json_error(array('message' => __('Please complete booking verification before confirming.', 'yatoon-booking-system')));
            }
            $expected_key = md5($required_channel . $customer_email . $customer_phone . 'verified');
            if (!$otp_verified_key || !hash_equals($expected_key, $otp_verified_key) || !get_transient('yatoon_otp_verified_' . $expected_key)) {
                wp_send_json_error(array('message' => __('Please complete booking verification before confirming.', 'yatoon-booking-system')));
            }
        }
        if ($policy_ack_required && !$policy_acknowledged) {
            wp_send_json_error(array('message' => __('Please review and acknowledge the cancellation / no-show policy before booking.', 'yatoon-booking-system')));
        }
        if ($consent_required && (!$consent_accepted || $consent_signature === '')) {
            wp_send_json_error(array('message' => __('Please read and sign the consent form before booking.', 'yatoon-booking-system')));
        }

        $design_reference_fields = array(
            'design_attachment_id' => null,
            'design_image_url'     => null,
            'design_upload_token'  => '',
        );

        $consent_booking_fields = array_merge( array(
            'consent_accepted'      => $consent_required ? $consent_accepted : 0,
            'consent_signature'     => $consent_required ? $consent_signature : null,
            'consent_signed_at'     => ($consent_required && $consent_accepted) ? current_time('mysql', true) : null,
            'consent_text_snapshot' => $consent_required ? wp_strip_all_tags(get_option('yatoon_consent_text', '')) : null,
            'policy_acknowledged'   => $policy_should_save ? $policy_acknowledged : 0,
            'policy_acknowledged_at'=> ($policy_should_save && $policy_acknowledged) ? current_time('mysql', true) : null,
            'policy_text_snapshot'  => $policy_should_save ? wp_strip_all_tags(get_option('yatoon_policy_text', '')) : null,
        ), $deposit_fields, $design_reference_fields );

        // ------------------------------------------------------------
        // ANTI-ABUSE PROTECTION LAYER
        // ------------------------------------------------------------

        // -- Layer 1: Phone number format validation -----------------------
        $ybs_phone_required = get_option('yatoon_phone_required','1') === '1';
        $ybs_phone_mode     = get_option('yatoon_phone_mode','us_only');
        $phone_digits = preg_replace('/\D/', '', $customer_phone);

        if ($ybs_phone_mode === 'us_only') {
            // US-only: accept 10-digit or 11-digit starting with 1
            if (strlen($phone_digits) === 11 && substr($phone_digits, 0, 1) === '1') {
                $phone_digits = substr($phone_digits, 1);
            }
            if ($ybs_phone_required && strlen($phone_digits) !== 10) {
                wp_send_json_error(array('message' => __('Please enter a valid 10-digit US phone number.', 'yatoon-booking-system')));
            }
            // -- US number quality checks ----------------------------------
            if ( strlen($phone_digits) === 10 ) {
                $area_code = substr($phone_digits, 0, 3);
                $exchange  = substr($phone_digits, 3, 3);
                // Area code cannot start with 0 or 1
                if ( in_array($area_code[0], array('0','1'), true) ) {
                    wp_send_json_error(array('message' => __('Please enter a valid US phone number.', 'yatoon-booking-system')));
                }
                // Exchange cannot start with 0 or 1
                if ( in_array($exchange[0], array('0','1'), true) ) {
                    wp_send_json_error(array('message' => __('Please enter a valid US phone number.', 'yatoon-booking-system')));
                }
                // Reject all-same digits (e.g. 1111111111, 0000000000)
                if ( strlen(count_chars($phone_digits, 3)) === 1 ) {
                    wp_send_json_error(array('message' => __('Please enter a valid US phone number.', 'yatoon-booking-system')));
                }
                // Reject known fake/sequential numbers
                $fake_numbers = array('1234567890','0123456789','9876543210','5555555555','1231231234','0000000000','1111111111','2222222222','3333333333','4444444444','6666666666','7777777777','8888888888','9999999999');
                if ( in_array($phone_digits, $fake_numbers, true) ) {
                    wp_send_json_error(array('message' => __('Please enter a valid US phone number.', 'yatoon-booking-system')));
                }
            }
            $customer_phone_normalized = $phone_digits;
        } else {
            // International mode: accept E.164 (+countrycode + digits) or any digit string >= 6 digits
            // If submitted as E.164 (starts with +), keep as-is; otherwise accept raw digits
            if (strlen($customer_phone) > 0 && $customer_phone[0] === '+') {
                // E.164 format from intl-tel-input - store full value, normalize last 10 digits for blacklist checks
                if ($ybs_phone_required && strlen($phone_digits) < 6) {
                    wp_send_json_error(array('message' => __('Please enter a valid phone number.', 'yatoon-booking-system')));
                }
                $customer_phone_normalized = strlen($phone_digits) >= 10 ? substr($phone_digits, -10) : $phone_digits;
            } else {
                if ($ybs_phone_required && strlen($phone_digits) < 6) {
                    wp_send_json_error(array('message' => __('Please enter a valid phone number.', 'yatoon-booking-system')));
                }
                $customer_phone_normalized = strlen($phone_digits) >= 10 ? substr($phone_digits, -10) : $phone_digits;
            }
        }

        // -- Layer 1.5: Phone authenticity check (Twilio Lookup, optional) --
        // Confirms the number is a real, dialable number (and optionally that
        // it's a mobile line that can receive SMS) - no extra step for the
        // customer, this runs silently in the background of their submit.
        if (get_option('yatoon_phone_lookup_enabled', '0') === '1' && !empty($customer_phone) && class_exists('YATOON_Twilio')) {
            $lookup = YATOON_Twilio::lookup_validate($customer_phone);
            if ($lookup['checked'] && !$lookup['valid']) {
                wp_send_json_error(array('message' => __('Please enter a valid, active phone number.', 'yatoon-booking-system')));
            }
            if ($lookup['checked'] && get_option('yatoon_phone_lookup_block_voip', '0') === '1' && $lookup['line_type'] === 'nonFixedVoip') {
                wp_send_json_error(array('message' => __('Please enter a mobile or landline phone number (not a VOIP number).', 'yatoon-booking-system')));
            }
        }

        // -- Layer 2: Blacklist check (phone, email, IP) --------------------
        $client_ip        = $this->get_client_ip();
        $blacklist_phones = array_filter(array_map('trim', explode("\n", get_option('yatoon_blacklist_phones', ''))));
        $blacklist_emails = array_filter(array_map('trim', explode("\n", get_option('yatoon_blacklist_emails', ''))));
        $blacklist_ips    = array_filter(array_map('trim', explode("\n", get_option('yatoon_blacklist_ips', ''))));

        if (in_array($customer_phone_normalized, array_map(fn($p) => preg_replace('/\D/', '', $p), $blacklist_phones), true)) {
            wp_send_json_error(array('message' => __('Unable to complete this booking. Please call us directly.', 'yatoon-booking-system')));
        }
        if (in_array(strtolower($customer_email), array_map('strtolower', $blacklist_emails), true)) {
            wp_send_json_error(array('message' => __('Unable to complete this booking. Please call us directly.', 'yatoon-booking-system')));
        }
        if (!empty($client_ip) && in_array($client_ip, $blacklist_ips, true)) {
            wp_send_json_error(array('message' => __('Unable to complete this booking. Please call us directly.', 'yatoon-booking-system')));
        }

        // -- Layer 3: IP rate limiting -------------------------------------
        if (!empty($client_ip)) {
            $rate_key     = 'yatoon_rate_' . md5($client_ip);
            $rate_data    = get_transient($rate_key);
            $rate_count   = $rate_data ? intval($rate_data) : 0;
            $rate_limit   = intval(get_option('yatoon_rate_limit_per_ip', 10));
            $rate_minutes = intval(get_option('yatoon_rate_limit_minutes', 10));
            if ($rate_count >= $rate_limit) {
                wp_send_json_error(array('message' => sprintf(
                    __('Too many booking attempts. Please wait %d minutes and try again, or call us directly.', 'yatoon-booking-system'),
                    $rate_minutes
                )));
            }
            set_transient($rate_key, $rate_count + 1, $rate_minutes * MINUTE_IN_SECONDS);
        }

        // -- Layer 4: Per-phone future booking limit ------------------------
        // A phone number may not have more than N upcoming bookings.
        // Set to 0 in Protection settings to disable this check entirely.
        {
            global $wpdb;
            $tbl_bk_chk  = $wpdb->prefix . 'yatoon_bookings';
            $max_future  = intval(get_option('yatoon_max_future_bookings_per_phone', 0));

            if ($max_future > 0) {
                // DB stores start_time in UTC - compare against UTC now
                $now_str_chk = gmdate('Y-m-d H:i:s');

                // Auto-sync: mark past appointments as completed so they never block new bookings
                yatoon_db_query($wpdb->prepare(
                    "UPDATE $tbl_bk_chk
                        SET status = 'completed'
                      WHERE end_time < %s
                        AND status NOT IN ('cancelled','no_show','completed')",
                    $now_str_chk
                ));

                // Count future confirmed/pending bookings for this phone.
                // BUG FIX (v4.5.14): the previous query used REGEXP_REPLACE (only
                // available on MySQL 8+ / MariaDB 10.0.5+) and LIKE %digits%
                // which falsely matched any phone whose last digits happened to
                // contain the same substring. We now strip the common formatting
                // chars in pure SQL and compare the last 10 digits exactly.
                $future_count = yatoon_db_get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM $tbl_bk_chk
                      WHERE RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(customer_phone,'-',''),' ',''),'(',''),')',''),'+',''),'.',''),'\t',''), 10) = %s
                        AND start_time > %s
                        AND status NOT IN ('cancelled','no_show','completed')",
                    $customer_phone_normalized,
                    $now_str_chk
                ));
                if (intval($future_count) >= $max_future) {
                    wp_send_json_error(array('message' => sprintf(
                        __('You already have %d upcoming appointments. Please cancel an existing appointment before booking a new one, or call us.', 'yatoon-booking-system'),
                        intval($future_count)
                    )));
                }
            }
        }
        // ------------------------------------------------------------
        // END ANTI-ABUSE PROTECTION
        // ------------------------------------------------------------

        global $wpdb;
        $design_upload_service_ids = array();
        $design_upload_option_ids = $selected_options;
        if (!empty($_POST['services_json'])) {
            $design_upload_services = json_decode(wp_unslash($_POST['services_json']), true);
            if (is_array($design_upload_services)) {
                foreach ($design_upload_services as $design_upload_service) {
                    if (!empty($design_upload_service['service_id'])) {
                        $design_upload_service_ids[] = absint($design_upload_service['service_id']);
                    }
                    if (!empty($design_upload_service['option_ids']) && is_array($design_upload_service['option_ids'])) {
                        $design_upload_option_ids = array_merge(
                            $design_upload_option_ids,
                            array_map('absint', $design_upload_service['option_ids'])
                        );
                    }
                }
            }
        }
        if (empty($design_upload_service_ids) && $service_id) {
            $design_upload_service_ids[] = absint($service_id);
        }
        $design_upload_service_ids = array_values(array_unique(array_filter($design_upload_service_ids)));
        $design_upload_option_ids = array_values(array_unique(array_filter($design_upload_option_ids)));
        $design_upload_allowed = false;
        if (!empty($design_upload_service_ids)) {
            $design_placeholders = implode(',', array_fill(0, count($design_upload_service_ids), '%d'));
            $design_upload_allowed = (bool) yatoon_db_get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->prefix}yatoon_services
                 WHERE id IN ($design_placeholders) AND allow_design_upload = 1",
                $design_upload_service_ids
            ));
        }
        if (!$design_upload_allowed && !empty($design_upload_option_ids)) {
            $design_option_placeholders = implode(',', array_fill(0, count($design_upload_option_ids), '%d'));
            $design_option_query_args = $design_upload_option_ids;
            $design_option_query_args[] = '%nail art%';
            $design_option_query_args[] = '%custom design%';
            $design_upload_allowed = (bool) yatoon_db_get_var($wpdb->prepare(
                "SELECT COUNT(*)
                 FROM {$wpdb->prefix}yatoon_service_options option_row
                 LEFT JOIN {$wpdb->prefix}yatoon_services linked_service
                    ON linked_service.square_id = option_row.square_id
                 LEFT JOIN {$wpdb->prefix}yatoon_service_options source_option
                    ON source_option.square_id = option_row.square_id
                   AND source_option.id != option_row.id
                 LEFT JOIN {$wpdb->prefix}yatoon_services source_service
                    ON source_service.id = source_option.service_id
                 WHERE option_row.id IN ($design_option_placeholders)
                   AND (
                        linked_service.allow_design_upload = 1
                        OR source_service.allow_design_upload = 1
                        OR LOWER(option_row.name) LIKE %s
                        OR LOWER(option_row.name) LIKE %s
                   )",
                $design_option_query_args
            ));
        }
        if (get_option('yatoon_allow_design_upload', '1') === '1' && $design_upload_allowed) {
            $design_reference_fields = $this->resolve_design_reference_upload();
            $consent_booking_fields  = array_merge($consent_booking_fields, $design_reference_fields);
        }

        $table_services        = $wpdb->prefix . 'yatoon_services';
        $table_staff           = $wpdb->prefix . 'yatoon_staff';
        $table_bookings        = $wpdb->prefix . 'yatoon_bookings';
        $table_booking_options = $wpdb->prefix . 'yatoon_booking_options';

        $service = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_services WHERE id = %d", $service_id));
        if (!$service) {
            wp_send_json_error(array('message' => __('Invalid service selection', 'yatoon-booking-system')));
        }

        // Staff: local mode must stay inside WordPress. Square availability checks
        // only run when the active platform is Square.
        $platform = sanitize_key((string) get_option('yatoon_active_platform', 'local'));
        if (!in_array($platform, array('local', 'square', 'vagaro'), true)) {
            $platform = 'local';
        }
        $staff = null;
        if ($platform !== 'square') {
            if ($staff_id > 0) {
                $staff = yatoon_db_get_row($wpdb->prepare(
                    "SELECT * FROM $table_staff WHERE id = %d AND is_active = 1 AND is_bookable = 1",
                    $staff_id
                ));
            }

            if (!$staff) {
                $table_ss_local = $wpdb->prefix . 'yatoon_staff_services';
                $eligible_local = yatoon_db_get_results($wpdb->prepare(
                    "SELECT s.* FROM $table_staff s
                     INNER JOIN $table_ss_local ss ON s.id = ss.staff_id
                     WHERE ss.service_id = %d AND s.is_active = 1 AND s.is_bookable = 1
                     ORDER BY s.sort_order ASC, s.name ASC",
                    $service_id
                ));

                if (empty($eligible_local)) {
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
                    $eligible_local = yatoon_db_get_results(
                        "SELECT * FROM $table_staff WHERE is_active = 1 AND is_bookable = 1 ORDER BY sort_order ASC, name ASC"
                    );
                }

                if (!empty($eligible_local)) {
                    $staff = $eligible_local[0];
                    $staff_id = intval($staff->id);
                }
            }
        }
        if ($platform === 'square' && $staff_id === 0) {
            $staff_map_json_req = isset($_POST['staff_map_json']) ? wp_unslash($_POST['staff_map_json']) : '';
            $staff_map_req = $staff_map_json_req ? json_decode($staff_map_json_req, true) : null;
            if (is_array($staff_map_req) && !empty($staff_map_req[0])) {
                $mapped_square_staff_id = sanitize_text_field($staff_map_req[0]);
                $mapped_staff = yatoon_db_get_row($wpdb->prepare(
                    "SELECT * FROM $table_staff WHERE square_id = %s AND is_active = 1 AND is_bookable = 1 LIMIT 1",
                    $mapped_square_staff_id
                ));
                if ($mapped_staff) {
                    $staff = $mapped_staff;
                    $staff_id = intval($mapped_staff->id);
                }
            }
        }
        if ($platform === 'square' && $staff_id > 0) {
            $staff = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_staff WHERE id = %d", $staff_id));
            // For named staff, verify with Square they are actually available (catches blocked/off days)
            if ($staff) {
                $ns_var_id = null;
                $table_opts_ns = $wpdb->prefix . 'yatoon_service_options';
                if (!empty($selected_options)) {
                    $ids_ns = implode(',', array_map('intval', $selected_options));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $ns_var = yatoon_db_get_row("SELECT square_id FROM $table_opts_ns WHERE id IN ($ids_ns) AND square_id != '' ORDER BY CASE WHEN group_name = 'Add-on' THEN 1 ELSE 0 END, sort_order ASC, id ASC LIMIT 1");
                    if ($ns_var) $ns_var_id = $ns_var->square_id;
                }
                if (!$ns_var_id) {
                    $ns_var = yatoon_db_get_row($wpdb->prepare(
                        "SELECT square_id FROM $table_opts_ns WHERE service_id=%d AND square_id != '' LIMIT 1",
                        $service_id
                    ));
                    if ($ns_var) $ns_var_id = $ns_var->square_id;
                }
                if ($ns_var_id) {
                    $ns_start_dt = new DateTime($start_at);
                    $ns_start_ts = $ns_start_dt->getTimestamp();
                    $ns_dur      = intval($service->duration_minutes ?: 60);
                    $ns_win_s    = clone $ns_start_dt; $ns_win_s->setTimezone(new DateTimeZone('UTC'));
                    $ns_win_e    = clone $ns_start_dt; $ns_win_e->modify('+' . ($ns_dur + 61) . ' minutes')->setTimezone(new DateTimeZone('UTC'));
                    $ns_s_iso    = $ns_win_s->format('Y-m-d\TH:i:s\Z');
                    $ns_e_iso    = $ns_win_e->format('Y-m-d\TH:i:s\Z');

                    // Resolve add-on variation IDs for multi-segment verification
                    $ns_addon_var_ids = array();
                    if (!empty($selected_options)) {
                        $table_opts_ns2 = $wpdb->prefix . 'yatoon_service_options';
                        $ns_opt_ids_str = implode(',', array_map('intval', $selected_options));
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                        $ns_addon_rows = yatoon_db_get_results(
                            "SELECT square_id FROM $table_opts_ns2
                             WHERE id IN ($ns_opt_ids_str) AND square_id != ''
                               AND group_name = 'Add-on'"
                        );
                        foreach ((array)$ns_addon_rows as $ns_ar) {
                            $ns_addon_var_ids[] = $ns_ar->square_id;
                        }
                    }

                    if (!empty($ns_addon_var_ids)) {
                        // Multi-segment: main + add-ons for this specific staff
                        $ns_segs = array(array('variation_id' => $ns_var_id, 'team_member_id' => $staff->square_id));
                        foreach ($ns_addon_var_ids as $ns_av_id) {
                            $ns_segs[] = array('variation_id' => $ns_av_id, 'team_member_id' => $staff->square_id);
                        }
                        $ns_resp = $this->square_api->search_availability_multi_segment($ns_segs, $ns_s_iso, $ns_e_iso);
                    } else {
                        $ns_resp = $this->square_api->search_availability($ns_var_id, $ns_s_iso, $ns_e_iso, $staff->square_id);
                    }
                    $ns_ok = false;
                    if (!is_wp_error($ns_resp) && !empty($ns_resp['availabilities'])) {
                        foreach ($ns_resp['availabilities'] as $ns_av) {
                            if (empty($ns_av['start_at'])) continue;
                            if (abs((new DateTime($ns_av['start_at']))->getTimestamp() - $ns_start_ts) <= 15 * 60) {
                                $ns_ok = true; break;
                            }
                        }
                    }
                    if (!$ns_ok) {
                        wp_send_json_error(array(
                            'message'  => __('Sorry, the selected staff member is not available at this time. Please choose a different time or staff.', 'yatoon-booking-system'),
                            'no_staff' => true,
                        ));
                        return;
                    }
                }
            }
        }
        if ($platform === 'square' && !$staff) {
            // Any-staff: ask Square directly who is available at this time.
            // Square knows working hours, personal events (Busy blocks), and existing bookings.
            // We trust Square's answer completely - same principle as Pass2.
            $table_ss_sg = $wpdb->prefix . 'yatoon_staff_services';
            $eligible_sg = yatoon_db_get_results($wpdb->prepare(
                "SELECT s.* FROM $table_staff s
                 INNER JOIN $table_ss_sg ss ON s.id = ss.staff_id
                 WHERE ss.service_id = %d AND s.is_active=1 AND s.is_bookable=1
                 ORDER BY s.sort_order ASC, s.name ASC",
                $service_id
            ));
            if (empty($eligible_sg)) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $eligible_sg = yatoon_db_get_results(
                    "SELECT * FROM $table_staff WHERE is_active=1 AND is_bookable=1 ORDER BY sort_order ASC, name ASC"
                );
            }

            // Get variation ID for this service (needed for search_availability)
            // The column is 'square_id' in yatoon_service_options (not 'square_variation_id')
            $sg_var_id = null;
            $table_opts_sg = $wpdb->prefix . 'yatoon_service_options';
            if (!empty($selected_options)) {
                $ids_sg = implode(',', array_map('intval', $selected_options));
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $sg_var = yatoon_db_get_row("SELECT square_id FROM $table_opts_sg WHERE id IN ($ids_sg) AND square_id != '' ORDER BY CASE WHEN group_name = 'Add-on' THEN 1 ELSE 0 END, sort_order ASC, id ASC LIMIT 1");
                if ($sg_var) $sg_var_id = $sg_var->square_id;
            }
            if (!$sg_var_id) {
                $sg_var = yatoon_db_get_row($wpdb->prepare(
                    "SELECT square_id FROM $table_opts_sg WHERE service_id=%d AND square_id != '' LIMIT 1",
                    $service_id
                ));
                if ($sg_var) $sg_var_id = $sg_var->square_id;
            }

            // Build query window: [start_at, start_at + dur + 61min] in UTC ISO format
            $sg_svc_dur = intval($service->duration_minutes ?: 60);
            $sg_start_dt = new DateTime($start_at);
            $sg_start_ts = $sg_start_dt->getTimestamp();
            $sg_win_s = clone $sg_start_dt; $sg_win_s->setTimezone(new DateTimeZone('UTC'));
            $sg_win_e = clone $sg_start_dt; $sg_win_e->modify('+' . ($sg_svc_dur + 61) . ' minutes')->setTimezone(new DateTimeZone('UTC'));
            $sg_s_iso = $sg_win_s->format('Y-m-d\TH:i:s\Z');
            $sg_e_iso = $sg_win_e->format('Y-m-d\TH:i:s\Z');

            if ($sg_var_id) {
                // Resolve add-on variation IDs for multi-segment check (if any add-ons selected)
                $sg_addon_var_ids = array();
                if (!empty($selected_options)) {
                    $table_opts_sg2 = $wpdb->prefix . 'yatoon_service_options';
                    $sg_opt_ids_str = implode(',', array_map('intval', $selected_options));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $sg_addon_rows = yatoon_db_get_results(
                        "SELECT square_id, duration_minutes FROM $table_opts_sg2
                         WHERE id IN ($sg_opt_ids_str) AND square_id != ''
                           AND group_name = 'Add-on'"
                    );
                    foreach ((array)$sg_addon_rows as $sg_addon_row) {
                        $sg_addon_var_ids[] = $sg_addon_row->square_id;
                    }
                }

                // Step 1: Query Square without specifying a team member to get the whitelist
                // of truly available staff (respects working hours + All-Day Personal Events).
                // If add-ons are present, use multi-segment so Square validates the full window.
                if (!empty($sg_addon_var_ids)) {
                    $sg_any_segs = array(array('variation_id' => $sg_var_id));
                    foreach ($sg_addon_var_ids as $addon_vid) {
                        $sg_any_segs[] = array('variation_id' => $addon_vid);
                    }
                    $sg_any_resp = $this->square_api->search_availability_multi_segment($sg_any_segs, $sg_s_iso, $sg_e_iso);
                } else {
                    $sg_any_resp = $this->square_api->search_availability($sg_var_id, $sg_s_iso, $sg_e_iso);
                }
                $sq_available_ids = array();
                if (!is_wp_error($sg_any_resp) && !empty($sg_any_resp['availabilities'])) {
                    foreach ($sg_any_resp['availabilities'] as $sg_av) {
                        if (empty($sg_av['start_at'])) continue;
                        $sg_av_ts = (new DateTime($sg_av['start_at']))->getTimestamp();
                        if (abs($sg_av_ts - $sg_start_ts) <= 15 * 60) {
                            foreach ($sg_av['appointment_segments'] ?? [] as $seg) {
                                $tid = $seg['team_member_id'] ?? '';
                                if ($tid) $sq_available_ids[$tid] = true;
                            }
                        }
                    }
                }
                yatoon_safe_log("SBS single-svc: Square available team IDs" . (!empty($sg_addon_var_ids) ? ' (multi-seg)' : '') . ": " . implode(', ', array_keys($sq_available_ids)));

                // Step 2: Ask Square directly for each eligible staff, skip if not in whitelist
                foreach ($eligible_sg as $cand_sg) {
                    if (empty($cand_sg->square_id)) continue;

                    // Must be in the whitelist - filters out All-Day Blocked staff
                    if (!empty($sq_available_ids) && !isset($sq_available_ids[$cand_sg->square_id])) {
                        yatoon_safe_log("SBS single-svc: {$cand_sg->name} - blocked (not in Square available list)");
                        continue;
                    }

                    // If add-ons present, verify with multi-segment query per staff
                    if (!empty($sg_addon_var_ids)) {
                        $sg_cand_segs = array(array('variation_id' => $sg_var_id, 'team_member_id' => $cand_sg->square_id));
                        foreach ($sg_addon_var_ids as $addon_vid) {
                            $sg_cand_segs[] = array('variation_id' => $addon_vid, 'team_member_id' => $cand_sg->square_id);
                        }
                        $sg_resp = $this->square_api->search_availability_multi_segment($sg_cand_segs, $sg_s_iso, $sg_e_iso);
                    } else {
                        $sg_resp = $this->square_api->search_availability($sg_var_id, $sg_s_iso, $sg_e_iso, $cand_sg->square_id);
                    }
                    if (is_wp_error($sg_resp) || empty($sg_resp['availabilities'])) {
                        yatoon_safe_log("SBS single-svc: {$cand_sg->name} - Square says not available");
                        continue;
                    }
                    foreach ($sg_resp['availabilities'] as $sg_av) {
                        if (empty($sg_av['start_at'])) continue;
                        $sg_av_ts = (new DateTime($sg_av['start_at']))->getTimestamp();
                        if (abs($sg_av_ts - $sg_start_ts) <= 15 * 60) {
                            $staff = $cand_sg;
                            yatoon_safe_log("SBS single-svc: {$cand_sg->name} - Square confirmed available");
                            break 2;
                        }
                    }
                    yatoon_safe_log("SBS single-svc: {$cand_sg->name} - no slot near requested time");
                }
            } else {
                // No variation ID - fallback to first eligible (Square will reject if unavailable)
                $staff = !empty($eligible_sg) ? $eligible_sg[0] : null;
                if ($staff) yatoon_safe_log("SBS single-svc: no variation ID, fallback to {$staff->name}");
            }
        }
        if (!$staff) {
            wp_send_json_error(array(
                'message'      => __('No staff available for this service. Please contact us.', 'yatoon-booking-system'),
                'no_staff'     => true,
            ));
        }

        // Service option price/duration
        $total_price    = floatval($service->price);
        $total_duration = intval($service->duration_minutes);

        if (!empty($selected_options)) {
            $table_options = $wpdb->prefix . 'yatoon_service_options';
            $ids_str = implode(',', array_map('intval', $selected_options));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $options = yatoon_db_get_results("SELECT * FROM $table_options WHERE id IN ($ids_str) ORDER BY sort_order ASC, id ASC");
            if (!empty($options)) {
                $main_opt_set = false;
                foreach ($options as $opt) {
                    $is_addon = (isset($opt->group_name) && $opt->group_name === 'Add-on');
                    if (!$is_addon && !$main_opt_set) {
                        // Main option: sets the base duration (replaces service default)
                        if (intval($opt->duration_minutes) > 0) {
                            $total_duration = intval($opt->duration_minutes);
                        }
                        $total_price += floatval($opt->price_modifier);
                        $main_opt_set = true;
                    } elseif ($is_addon) {
                        // Add-on (e.g. Custom Design): add duration and price on top
                        $total_duration += intval($opt->duration_minutes);
                        $total_price    += floatval($opt->price_modifier);
                    }
                }
                // Fallback: if no main option found (all tagged as addon), process first as main
                if (!$main_opt_set && !empty($options)) {
                    $total_price += floatval($options[0]->price_modifier);
                    if (intval($options[0]->duration_minutes) > 0) {
                        $total_duration = intval($options[0]->duration_minutes);
                    }
                }
            }
        }

        $site_tz = wp_timezone();
        $start_time = new DateTime($start_at, $site_tz);
        $start_time->setTimezone($site_tz);
        $end_time   = clone $start_time;
        $end_time->modify("+{$total_duration} minutes");

        $platform = sanitize_key((string) get_option('yatoon_active_platform', 'local'));
        if (!in_array($platform, array('local', 'square', 'vagaro'), true)) {
            $platform = 'local';
        }
        if ($is_test_booking && $platform !== 'local') {
            wp_send_json_error(array(
                'message' => __('Test booking mode creates local WordPress bookings only. Switch Booking Platform to Local Only before running a test booking.', 'yatoon-booking-system'),
            ));
        }

        $local_booking_lock_key = false;
        if ($platform === 'local') {
            $local_booking_lock_key = $this->acquire_local_booking_lock($start_at, $booking_service_ids);
            if (!$local_booking_lock_key) {
                wp_send_json_error(array(
                    'message' => __('That time is being booked right now. Please wait a few seconds and try again.', 'yatoon-booking-system'),
                ));
            }
            register_shutdown_function(array($this, 'release_local_booking_lock'), $local_booking_lock_key);
        }

        // -- Atomically claim the coupon now that all validation has passed.
        //    Concurrent bookings cannot blow past usage_limit because
        //    claim_coupon() does the limit check + increment in one SQL.
        //    On any failure path BELOW this point that exits without inserting
        //    a booking row, release_coupon() must be called (see insert error
        //    branches and the wrapper at the end of create_booking).
        if ( $coupon_code !== '' && class_exists( 'YATOON_Growth_Modules' ) ) {
            $claimed_coupon = YATOON_Growth_Modules::claim_coupon( $coupon_code );
            if ( ! $claimed_coupon ) {
                wp_send_json_error( array(
                    'message' => __( 'This coupon is no longer available (expired or usage limit reached). Please remove it and try again.', 'yatoon-booking-system' ),
                ) );
            }
        }

        // BUG FIX (v2.92): Re-enabled staff overlap check (single-service).
        // Covers both named-staff and any-staff bookings.
        // Prevents double-bookings when two customers submit simultaneously.
        if (!empty($staff->id)) {
            $table_bk_sg = $wpdb->prefix . 'yatoon_bookings';
            $ov_s = $start_time->format('Y-m-d H:i:s');
            $ov_e = $end_time->format('Y-m-d H:i:s');
            $ov_conflict = yatoon_db_get_var($wpdb->prepare(
                "SELECT id FROM $table_bk_sg
                 WHERE staff_id = %d
	                   AND status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')
                   AND start_time < %s
                   AND end_time   > %s
                 LIMIT 1",
                intval($staff->id), $ov_e, $ov_s
            ));
            if ($ov_conflict) {
                // DB shows conflict but DB may be stale - log and let Square decide.
                yatoon_safe_log("SBS single-svc overlap: DB conflict for staff {$staff->name} at {$ov_s}-{$ov_e}, proceeding - Square is the final arbiter.");
            }
        }
        // end single-service overlap check

        // Local / Vagaro: validate slot -> store locally -> send email
        // -- VAGARO MODE ------------------------------------------------------
        if ( $platform === 'vagaro' ) {
            if ( ! class_exists( 'YATOON_Vagaro_API' ) ) {
                require_once SBS_PLUGIN_DIR . 'includes/class-yatoon-vagaro-api.php';
            }
            $vagaro = new YATOON_Vagaro_API();
            global $wpdb;

            $customer_name  = sanitize_text_field( wp_unslash( $_POST['customer_name']  ?? '' ) );
            $customer_email = sanitize_email(       wp_unslash( $_POST['customer_email'] ?? '' ) );
            $customer_phone = sanitize_text_field( wp_unslash( $_POST['customer_phone'] ?? '' ) );
            $service_id     = absint( $_POST['service_id'] ?? 0 );
            $staff_id       = absint( $_POST['staff_id']   ?? 0 );
            $start_at       = sanitize_text_field( wp_unslash( $_POST['start_at'] ?? '' ) );
            $notes          = sanitize_textarea_field( wp_unslash( $_POST['notes'] ?? '' ) );

            $svc   = yatoon_db_get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_services WHERE id = %d", $service_id ) ); // phpcs:ignore
            $staff = yatoon_db_get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_staff WHERE id = %d", $staff_id ) );     // phpcs:ignore

            if ( ! $svc ) {
                wp_send_json_error( array( 'message' => __( 'Service not found.', 'yatoon-booking-system' ) ) );
            }

            $vagaro_service_id  = $svc->square_id ?? '';
            $vagaro_employee_id = $staff ? ( $staff->square_id ?? '' ) : '';

            if ( empty( $vagaro_service_id ) ) {
                wp_send_json_error( array( 'message' => __( 'This service is not linked to Vagaro. Please contact us.', 'yatoon-booking-system' ) ) );
            }

            // Parse start time
            $start_dt = new DateTime( $start_at, wp_timezone() );
            $start_dt->setTimezone( wp_timezone() );
            $end_dt   = clone $start_dt;
            $end_dt->modify( '+' . intval( $svc->duration_minutes ) . ' minutes' );

            // Build Vagaro appointment payload
            $vagaro_payload = array(
                'serviceId'   => $vagaro_service_id,
                'startTime'   => $start_dt->format( 'Y-m-d\TH:i:s' ),
                'endTime'     => $end_dt->format( 'Y-m-d\TH:i:s' ),
                'clientName'  => $customer_name,
                'clientEmail' => $customer_email,
                'clientPhone' => $customer_phone,
                'notes'       => $notes,
            );
            if ( $vagaro_employee_id ) {
                $vagaro_payload['employeeId'] = $vagaro_employee_id;
            }

            $vagaro_resp = $vagaro->create_booking( $vagaro_payload );

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

            $vagaro_booking_id = $vagaro_resp['id'] ?? $vagaro_resp['appointmentId'] ?? '';

            // Save to local DB
            yatoon_db_insert( $wpdb->prefix . 'yatoon_bookings', array_merge( array( // phpcs:ignore
                'square_booking_id' => $vagaro_booking_id,
                'customer_name'     => $customer_name,
                'customer_email'    => $customer_email,
                'customer_phone'    => $customer_phone,
                'service_id'        => $service_id,
                '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'            => $initial_status,
                'notes'             => $notes,
                'total_price'       => $svc->price ?? 0,
                'square_synced'     => 1,
            ), $consent_booking_fields ) );
            $local_booking_id = $wpdb->insert_id;

            // Generate manage token
            $manage_token = '';
            if ( class_exists( 'Yatoon_Manage_Booking' ) && $local_booking_id ) {
                $manage_token = Yatoon_Manage_Booking::create_token( $local_booking_id );
            }

            // Send confirmation email
            if ( get_option( 'yatoon_send_confirmation_email', '1' ) === '1' && $customer_email ) {
                $booking_data = array(
                    'booking_id'    => $local_booking_id,
                    'customer_name' => $customer_name, 'customer_email' => $customer_email,
                    'customer_phone'=> $customer_phone, 'service_name'   => $svc->name,
                    'staff_name'    => $staff ? $staff->name : '', 'start_time' => $start_dt->format('Y-m-d H:i:s'),
                    'end_time'      => $end_dt->format('Y-m-d H:i:s'), 'total_price' => $svc->price ?? 0,
                    'notes'         => $notes, 'manage_token'  => $manage_token, 'selected_options' => array(),
                ) + $consent_booking_fields;
                do_action( 'yatoon_booking_created', $booking_data );
            }

            wp_send_json_success( array(
                'booking_id'    => $local_booking_id,
                'vagaro_id'     => $vagaro_booking_id,
                'manage_token'  => $manage_token,
                'manage_url'    => $manage_token ? Yatoon_Manage_Booking::get_manage_url( $manage_token ) : '',
                'service_name'  => $svc->name ?? '',
                'design_attachment_id' => $design_reference_fields['design_attachment_id'] ?? 0,
                'design_image_url' => $design_reference_fields['design_image_url'] ?? '',
                'design_upload_token' => $design_reference_fields['design_upload_token'] ?? '',
                'message'       => __( 'Booking confirmed!', 'yatoon-booking-system' ),
            ) );
        }


        if ($platform !== 'square') {
            // Local mode booking flow - mirrors Square flow but uses local DB
            // ------------------------------------------------------------
            $timezone_local  = ( wp_timezone_string() ?: 'America/Chicago' );
            $tz_obj_local    = new DateTimeZone($timezone_local);
            $start_time_obj  = new DateTime($start_at, $tz_obj_local);

            $table_options_local = $wpdb->prefix . 'yatoon_service_options';
            $table_ss_local      = $wpdb->prefix . 'yatoon_staff_services';

            // Helper: get eligible staff for a service
            $get_eligible_local = function($svc_id) use ($wpdb, $table_staff, $table_ss_local) {
                $assigned = yatoon_db_get_results($wpdb->prepare(
                    "SELECT s.* FROM $table_staff s
                     INNER JOIN $table_ss_local ss ON s.id = ss.staff_id
                     WHERE ss.service_id = %d AND s.is_active=1 AND s.is_bookable=1
                     ORDER BY s.sort_order ASC, s.name ASC",
                    $svc_id
                ));
                if (!empty($assigned)) return $assigned;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
                return yatoon_db_get_results("SELECT * FROM $table_staff WHERE is_active=1 AND is_bookable=1 ORDER BY sort_order ASC, name ASC");
            };

            $services_json_local  = isset($_POST['services_json']) ? wp_unslash($_POST['services_json']) : '';
            $multi_services_local = $services_json_local ? json_decode($services_json_local, true) : null;

            // 闁冲厜鍋撻柍鍏夊亾 MULTI-SERVICE LOCAL BOOKING 闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾
            if ($multi_services_local && is_array($multi_services_local) && count($multi_services_local) > 1) {

                // -- Staff assignment -----------------------------------------
                $assigned_local       = array();
                // Track staff usage per guest_label so different guests can share
                // the same staff member (they start at the same time, not sequentially).
                $used_staff_per_guest = array(); // guest_label => [staff_id, ...]

                // Pass 1: named staff
                foreach ($multi_services_local as $idx => $svc_data) {
                    $svc_stf     = intval($svc_data['staff_id'] ?? 0);
                    $guest_label = isset($svc_data['guest_label']) && $svc_data['guest_label'] !== '' ? $svc_data['guest_label'] : 'Me';
                    if ($svc_stf > 0) {
                        $stf_row = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_staff WHERE id = %d AND is_active=1 AND is_bookable=1", $svc_stf));
                        if ($stf_row) {
                            $assigned_local[$idx] = $stf_row;
                            $used_staff_per_guest[$guest_label][] = intval($stf_row->id);
                        }
                    }
                }

                // Pass 2: any staff - prefer unused staff, but never assign a staff
                // to two services whose time windows overlap (across any guest).
                // Pre-compute offset+duration per index so we can check overlaps.
	                {
	                    $pos_dur_local = array();
	                    $guest_off_tmp = array();
                    foreach ($multi_services_local as $idx => $svc_data) {
                        $gl  = isset($svc_data['guest_label']) && $svc_data['guest_label'] !== '' ? $svc_data['guest_label'] : 'Me';
                        $sid = intval($svc_data['service_id'] ?? 0);
                        $opts = array_map('intval', (array)($svc_data['option_ids'] ?? []));
	                        $dur = intval(yatoon_db_get_var($wpdb->prepare("SELECT duration_minutes FROM $table_services WHERE id = %d", $sid)) ?: 60);
	                        if (!empty($opts)) {
	                            $ids_pl = implode(',', array_map('intval', $opts));
	                            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	                            $opt_rows = yatoon_db_get_results("SELECT * FROM $table_options_local WHERE id IN ($ids_pl) ORDER BY sort_order ASC, id ASC");
	                            $main_set = false;
	                            foreach ($opt_rows as $opt_row) {
	                                $is_addon = (isset($opt_row->group_name) && $opt_row->group_name === 'Add-on');
	                                if (!$is_addon && !$main_set) {
	                                    if (intval($opt_row->duration_minutes) > 0) $dur = intval($opt_row->duration_minutes);
	                                    $main_set = true;
	                                } elseif ($is_addon) {
	                                    $dur += intval($opt_row->duration_minutes);
	                                }
	                            }
	                            if (!$main_set && !empty($opt_rows) && intval($opt_rows[0]->duration_minutes) > 0) {
	                                $dur = intval($opt_rows[0]->duration_minutes);
	                            }
	                        }
	                        $dur = max(1, $dur);
                        $off = $guest_off_tmp[$gl] ?? 0;
                        $pos_dur_local[$idx] = array('offset' => $off, 'duration' => $dur, 'guest_label' => $gl);
	                        $guest_off_tmp[$gl] = $off + $dur;
	                    }
	                }

	                $local_candidate_available_for_window = function($candidate_staff_id, $offset_min, $duration_min) use ($wpdb, $table_bookings, $start_time_obj, $tz_obj_local) {
	                    $candidate_staff_id = intval($candidate_staff_id);
	                    if ($candidate_staff_id <= 0) return false;

	                    $svc_start_check = clone $start_time_obj;
	                    if (intval($offset_min) > 0) $svc_start_check->modify('+' . intval($offset_min) . ' minutes');
	                    $svc_end_check = clone $svc_start_check;
	                    $svc_end_check->modify('+' . max(1, intval($duration_min)) . ' minutes');

	                    $slot_date = $svc_start_check->format('Y-m-d');
	                    $day_hours = YATOON_Business_Hours::get_day_hours($slot_date);
	                    if (!$day_hours) return false;

	                    $open_dt  = new DateTime($slot_date . ' ' . ($day_hours['start'] ?? '09:00'), $tz_obj_local);
	                    $close_dt = new DateTime($slot_date . ' ' . ($day_hours['end'] ?? '17:00'), $tz_obj_local);
	                    if ($svc_start_check < $open_dt || $svc_end_check > $close_dt) return false;

	                    if (!empty($day_hours['break_start']) && !empty($day_hours['break_end'])) {
	                        $break_start = new DateTime($slot_date . ' ' . $day_hours['break_start'], $tz_obj_local);
	                        $break_end   = new DateTime($slot_date . ' ' . $day_hours['break_end'], $tz_obj_local);
	                        if ($svc_start_check < $break_end && $svc_end_check > $break_start) return false;
	                    }

	                    $conflict = yatoon_db_get_var($wpdb->prepare(
	                        "SELECT id FROM $table_bookings
	                         WHERE staff_id = %d
	                           AND status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')
	                           AND start_time < %s
	                           AND end_time   > %s
	                         LIMIT 1",
	                        $candidate_staff_id,
	                        $svc_end_check->format('Y-m-d H:i:s'),
	                        $svc_start_check->format('Y-m-d H:i:s')
	                    ));
	                    if ($conflict) return false;

	                    return !YATOON_Business_Hours::has_staff_time_off_conflict($candidate_staff_id, $svc_start_check, $svc_end_check, $tz_obj_local);
	                };

	                $candidate_sets_local = array();
	                foreach ($multi_services_local as $idx => $svc_data) {
	                    $svc_id  = intval($svc_data['service_id'] ?? 0);
	                    $svc_stf = intval($svc_data['staff_id'] ?? 0);
	                    $candidates = array();
	                    if ($svc_stf > 0) {
	                        if (isset($assigned_local[$idx])) {
	                            $candidates[] = $assigned_local[$idx];
	                        }
	                    } else {
	                        $candidates = $get_eligible_local($svc_id);
	                    }
	                    $candidate_sets_local[$idx] = $candidates;
	                }

	                $assignment_is_available = function($candidate, $idx, $current_assignment) use ($pos_dur_local, $local_candidate_available_for_window) {
	                    $cid = intval($candidate->id);
	                    $cur_off = $pos_dur_local[$idx]['offset'] ?? 0;
	                    $cur_dur = $pos_dur_local[$idx]['duration'] ?? 60;
	                    $cur_end = $cur_off + $cur_dur;
	                    if (!$local_candidate_available_for_window($cid, $cur_off, $cur_dur)) {
	                        return false;
	                    }
	                    foreach ($current_assignment as $prev_idx => $prev_stf) {
	                        if (!$prev_stf || intval($prev_stf->id) !== $cid) {
	                            continue;
	                        }
	                        $prev_off = $pos_dur_local[$prev_idx]['offset'] ?? 0;
	                        $prev_end = $prev_off + ($pos_dur_local[$prev_idx]['duration'] ?? 60);
	                        if ($prev_off < $cur_end && $prev_end > $cur_off) {
	                            return false;
	                        }
	                    }
	                    return true;
	                };

	                $assigned_local = array();
	                $assign_indexes_local = array_keys($multi_services_local);
	                usort($assign_indexes_local, function($a, $b) use ($candidate_sets_local) {
	                    return count($candidate_sets_local[$a] ?? array()) <=> count($candidate_sets_local[$b] ?? array());
	                });
	                $try_assign_local = function($pos, $current_assignment) use (&$try_assign_local, $assign_indexes_local, $candidate_sets_local, $assignment_is_available) {
	                    if ($pos >= count($assign_indexes_local)) {
	                        return $current_assignment;
	                    }
	                    $idx = $assign_indexes_local[$pos];
	                    foreach (($candidate_sets_local[$idx] ?? array()) as $candidate) {
	                        if (!$assignment_is_available($candidate, $idx, $current_assignment)) {
	                            continue;
	                        }
	                        $next_assignment = $current_assignment;
	                        $next_assignment[$idx] = $candidate;
	                        $result = $try_assign_local($pos + 1, $next_assignment);
	                        if ($result !== false) {
	                            return $result;
	                        }
	                    }
	                    return false;
	                };
	                $assignment_result_local = $try_assign_local(0, array());
	                if ($assignment_result_local === false) {
	                    wp_send_json_error(array('message' => __('Sorry, that time slot is no longer available. Please choose another time.', 'yatoon-booking-system')));
	                }
	                $assigned_local = $assignment_result_local;

                // -- Phase 1: Calculate timing + validate ALL slots before saving 闁冲厜鍋撻柍鍏夊亾
                // Track time used per guest_label (guests run in parallel, services within
                // a guest run sequentially on their assigned staff).
                $guest_time_used_local    = array(); // guest_label => minutes offset from start
                $cumulative_min_local     = 0;
                $growth_reserved_packages = array();
                $prepared_bookings        = array();

                foreach ($multi_services_local as $idx => $svc_data) {
                    $svc_id   = intval($svc_data['service_id'] ?? 0);
                    $svc_opts = array_map('intval', (array)($svc_data['option_ids'] ?? []));

                    $svc_row = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_services WHERE id = %d", $svc_id));
                    if (!$svc_row) continue;

                    $stf_row = $assigned_local[$idx] ?? null;
                    if (!$stf_row) {
                        $fallback = $get_eligible_local($svc_id);
                        $stf_row  = !empty($fallback) ? $fallback[0] : null;
                    }
                    if (!$stf_row) continue;

                    // Duration from options
                    $svc_dur   = intval($svc_row->duration_minutes);
                    $svc_price = floatval($svc_row->price);
                    if (!empty($svc_opts)) {
                        $ids_pl = implode(',', array_map('intval', $svc_opts));
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                        $all_opts = yatoon_db_get_results("SELECT * FROM $table_options_local WHERE id IN ($ids_pl) ORDER BY sort_order ASC, id ASC");
                        $main_set = false;
                        foreach ($all_opts as $opt_row) {
                            $is_addon = (isset($opt_row->group_name) && $opt_row->group_name === 'Add-on');
                            if (!$is_addon && !$main_set) {
                                if (intval($opt_row->duration_minutes) > 0) $svc_dur = intval($opt_row->duration_minutes);
                                $svc_price += floatval($opt_row->price_modifier);
                                $main_set = true;
                            } elseif ($is_addon) {
                                $svc_dur   += intval($opt_row->duration_minutes);
                                $svc_price += floatval($opt_row->price_modifier);
                            }
                        }
                    }

                    // Timing: services within the same guest run sequentially;
                    // services for different guests run in parallel from the same start time.
                    $guest_label  = isset($svc_data['guest_label']) && $svc_data['guest_label'] !== '' ? $svc_data['guest_label'] : 'Me';
                    $guest_offset = $guest_time_used_local[$guest_label] ?? 0;
                    $svc_start    = clone $start_time_obj;
                    if ($guest_offset > 0) $svc_start->modify("+{$guest_offset} minutes");
                    $svc_end = clone $svc_start;
                    $svc_end->modify("+{$svc_dur} minutes");

                    // Validate slot: check business hours + existing DB bookings,
                    // but also check in-memory prepared_bookings (same staff, overlapping time)
                    // so that within-group bookings don't block each other.
                    $date_for_slot = $svc_start->format('Y-m-d');
                    $day_hours     = YATOON_Business_Hours::get_day_hours($date_for_slot);
                    if (!$day_hours) {
                        wp_send_json_error(array('message' => sprintf(
                            __('Sorry, %s is not available on that date. Please choose another time.', 'yatoon-booking-system'),
                            $svc_row->name
                        )));
                    }
                    // Check existing DB conflicts for this staff
                    $db_booked  = YATOON_Business_Hours::get_booked_slots_public($date_for_slot, intval($stf_row->id));
                    $db_conflict = false;
                    foreach ($db_booked as $b) {
                        $bs = new DateTime($b->start_time, $tz_obj_local);
                        $be = new DateTime($b->end_time,   $tz_obj_local);
                        if ($svc_start < $be && $svc_end > $bs) { $db_conflict = true; break; }
                    }
                    // Check in-memory conflicts (other bookings in this same group for the same staff)
                    $mem_conflict = false;
                    foreach ($prepared_bookings as $pb_check) {
                        if (intval($pb_check['stf_row']->id) !== intval($stf_row->id)) continue;
                        if ($svc_start < $pb_check['svc_end'] && $svc_end > $pb_check['svc_start']) {
                            $mem_conflict = true; break;
                        }
                    }
                    if ($db_conflict || $mem_conflict) {
                        wp_send_json_error(array('message' => sprintf(
                            __('Sorry, the time slot for %s is no longer available. Please choose another time.', 'yatoon-booking-system'),
                            $svc_row->name
                        )));
                    }

                    $guest_time_used_local[$guest_label] = $guest_offset + $svc_dur;
                    $cumulative_min_local = max($cumulative_min_local, $guest_time_used_local[$guest_label]);

                    $prepared_bookings[] = compact('svc_row', 'stf_row', 'svc_id', 'svc_opts', 'svc_dur', 'svc_price', 'svc_start', 'svc_end');
                }

                if (empty($prepared_bookings)) {
                    wp_send_json_error(array('message' => __('Failed to create booking. Please try again.', 'yatoon-booking-system')));
                }

                // -- Phase 2: All slots valid - save all bookings.
                $first_booking_id       = null;
                $all_services_info      = array();
                $group_booking_id_local = $booking_group_id ? $booking_group_id : wp_generate_uuid4();
                $service_order_local    = 0;

                foreach ($prepared_bookings as $pb) {
                    $svc_row   = $pb['svc_row'];
                    $stf_row   = $pb['stf_row'];
                    $svc_id    = $pb['svc_id'];
                    $svc_opts  = $pb['svc_opts'];
                    $svc_price = $pb['svc_price'];
                    $svc_start = $pb['svc_start'];
                    $svc_end   = $pb['svc_end'];

                    $booking_notes   = $notes;
                    $booking_benefit = array();
                    if (class_exists('YATOON_Growth_Modules')) {
                        $booking_benefit = YATOON_Growth_Modules::apply_customer_benefits_to_total($svc_price, $customer_email, $customer_phone, $growth_reserved_packages);
                        if (!empty($booking_benefit['type'])) {
                            $svc_price     = floatval($booking_benefit['total']);
                            $booking_notes = trim($booking_notes . "\n" . $booking_benefit['note']);
                        }
                    }
                    if ($claimed_coupon && $svc_price > 0) {
                        $is_fixed_coupon = ($claimed_coupon->discount_type === 'fixed');
                        if (!$is_fixed_coupon || $service_order_local === 0) {
                            $coupon_discount = YATOON_Growth_Modules::compute_coupon_discount_for($claimed_coupon, $svc_price);
                            if ($coupon_discount > 0) {
                                $svc_price     = max(0, round($svc_price - $coupon_discount, 2));
                                $booking_notes = trim($booking_notes . "\n" . sprintf('Coupon %s applied: -$%s', $coupon_code, number_format($coupon_discount, 2)));
                            }
                        }
                    }

                    $insert_result = yatoon_db_insert($table_bookings, array_merge(array(
                        'square_booking_id'  => null,
                        'customer_name'      => $customer_name,
                        'customer_email'     => $customer_email,
                        'customer_phone'     => $customer_phone,
                        'service_id'         => $svc_id,
                        'staff_id'           => $stf_row->id,
                        'start_time'         => $svc_start->format('Y-m-d H:i:s'),
                        'end_time'           => $svc_end->format('Y-m-d H:i:s'),
                        'status'             => $initial_status,
                        'notes'              => $booking_notes,
                        'tip_amount'         => $tip_amount,
                        'custom_field_values'=> $custom_field_values,
                        'total_price'        => $svc_price + $tip_amount,
                        'square_synced'      => 0,
                        'payment_status'     => 'unpaid',
                        'group_booking_id'   => $group_booking_id_local,
                        'service_order'      => $service_order_local,
                        'is_any_staff'       => 0,
                    ), $consent_booking_fields));

                    if ($insert_result === false) {
                        if (class_exists('YATOON_Growth_Modules')) {
                            if (!empty($booking_benefit['type'])) YATOON_Growth_Modules::release_customer_benefit($booking_benefit);
                            if ($claimed_coupon) YATOON_Growth_Modules::release_coupon($coupon_code);
                        }
                        wp_send_json_error(array('message' => __('Failed to create booking. Please try again.', 'yatoon-booking-system')));
                    }

                    $local_booking_id = $wpdb->insert_id;
                    if (class_exists('YATOON_Growth_Modules') && !empty($booking_benefit['type'])) {
                        YATOON_Growth_Modules::record_booking_benefit($booking_benefit, $local_booking_id);
                    }
                    if ($first_booking_id === null) $first_booking_id = $local_booking_id;
                    $service_order_local++;

                    if (!empty($svc_opts)) {
                        foreach ($svc_opts as $opt_id) {
                            yatoon_db_insert($table_booking_options, array('booking_id' => $local_booking_id, 'option_id' => $opt_id));
                        }
                    }

                    $all_services_info[] = array(
                        'service'    => $svc_row,
                        'staff'      => $stf_row,
                        'start_time' => $svc_start->format('Y-m-d H:i:s'),
                        'end_time'   => $svc_end->format('Y-m-d H:i:s'),
                        'price'      => $svc_price,
                    );
                }

	                if (empty($all_services_info)) {
	                    wp_send_json_error(array('message' => __('Failed to create booking. Please try again.', 'yatoon-booking-system')));
	                }

	                if ($initial_status === 'confirmed') {
	                    yatoon_db_update(
	                        $table_bookings,
	                        array('status' => 'confirmed'),
	                        array('group_booking_id' => $group_booking_id_local),
	                        array('%s'),
	                        array('%s')
	                    );
	                }
	
	                if ($coupon_code && class_exists('YATOON_Growth_Modules')) {
                    YATOON_Growth_Modules::mark_coupon_used($coupon_code);
                }

                // Manage token for first booking
                $manage_token = Yatoon_Manage_Booking::create_token($first_booking_id);
                $manage_url   = $manage_token ? Yatoon_Manage_Booking::get_manage_url($manage_token) : '';
                $calendar_url = YATOON_Calendar_Links::get_calendar_url($first_booking_id, $manage_token ?? '');

                // Build combined service name for email
                $combined_svc_names = array_map(function($si) { return $si['service']->name; }, $all_services_info);
                $combined_svc_obj   = clone $all_services_info[0]['service'];
                $combined_svc_obj->name = implode(', ', $combined_svc_names);

                $booking_data_multi = array(
                    'customer_name'      => $customer_name,
                    'customer_email'     => $customer_email,
                    'customer_phone'     => $customer_phone,
                    'start_time'         => $all_services_info[0]['start_time'],
                    'end_time'           => end($all_services_info)['end_time'],
                    'total_price'        => array_sum(array_column($all_services_info, 'price')) + $tip_amount,
                    'tip_amount'         => $tip_amount,
                    'custom_field_values'=> $custom_field_values,
                    'notes'              => $notes,
                    'services_info'      => $all_services_info,
                    'manage_token'       => $manage_token,
                    'booking_id'         => $first_booking_id,
                    'manage_url'         => $manage_url,
                    'calendar_url'       => $calendar_url,
                ) + $consent_booking_fields;

                if (get_option('yatoon_send_confirmation_email', '1') === '1') {
                    $this->send_confirmation_email($booking_data_multi, $combined_svc_obj, $all_services_info[0]['staff']);
                }
                $this->notify_admin_new_booking($booking_data_multi, $combined_svc_obj, $all_services_info[0]['staff']);
                YATOON_Twilio::send_customer_confirmation($booking_data_multi, $combined_svc_obj, $all_services_info[0]['staff']);
                YATOON_Twilio::send_admin_alert($booking_data_multi, $combined_svc_obj, $all_services_info[0]['staff']);

                do_action('yatoon_booking_created', $booking_data_multi);

                YATOON_PWA::send_push_to_admins(array(
                    'title'      => 'New Booking',
                    'body'       => $customer_name . ' - ' . $all_services_info[0]['service']->name . ' at ' . wp_date('g:i A', strtotime($all_services_info[0]['start_time'])),
                    'url'        => admin_url('admin.php?page=yatoon-booking-system'),
                    'booking_id' => $first_booking_id,
                ));

                wp_send_json_success(array(
                    'message'      => __('Booking created successfully!', 'yatoon-booking-system'),
                    'booking_id'   => $first_booking_id,
                    'manage_url'   => $manage_url,
                    'service_name' => $combined_svc_obj->name ?? '',
                    'calendar_url' => $calendar_url,
                    'square_synced'=> false,
                    'mode'         => 'local',
                    'design_attachment_id' => $design_reference_fields['design_attachment_id'] ?? 0,
                    'design_image_url' => $design_reference_fields['design_image_url'] ?? '',
                    'design_upload_token' => $design_reference_fields['design_upload_token'] ?? '',
                ));
            }

	            // 闁冲厜鍋撻柍鍏夊亾 SINGLE-SERVICE LOCAL BOOKING 闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋撻柍鍏夊亾闁冲厜鍋?
	            // Validate the final service window using the actual selected option/add-on duration.
	            $local_slot_is_available = function($candidate_staff_id) use ($wpdb, $table_bookings, $start_time, $end_time, $tz_obj_local) {
	                $candidate_staff_id = intval($candidate_staff_id);
	                if ($candidate_staff_id <= 0) return false;

	                $slot_date = $start_time->format('Y-m-d');
	                $day_hours = YATOON_Business_Hours::get_day_hours($slot_date);
	                if (!$day_hours) return false;

	                $open_dt  = new DateTime($slot_date . ' ' . ($day_hours['start'] ?? '09:00'), $tz_obj_local);
	                $close_dt = new DateTime($slot_date . ' ' . ($day_hours['end'] ?? '17:00'), $tz_obj_local);
	                if ($start_time < $open_dt || $end_time > $close_dt) return false;

	                if (!empty($day_hours['break_start']) && !empty($day_hours['break_end'])) {
	                    $break_start = new DateTime($slot_date . ' ' . $day_hours['break_start'], $tz_obj_local);
	                    $break_end   = new DateTime($slot_date . ' ' . $day_hours['break_end'], $tz_obj_local);
	                    if ($start_time < $break_end && $end_time > $break_start) return false;
	                }

	                $conflict = yatoon_db_get_var($wpdb->prepare(
	                    "SELECT id FROM $table_bookings
	                     WHERE staff_id = %d
	                       AND status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')
	                       AND start_time < %s
	                       AND end_time   > %s
	                     LIMIT 1",
	                    $candidate_staff_id,
	                    $end_time->format('Y-m-d H:i:s'),
	                    $start_time->format('Y-m-d H:i:s')
	                ));
	                if ($conflict) return false;

	                return !YATOON_Business_Hours::has_staff_time_off_conflict($candidate_staff_id, $start_time, $end_time, $tz_obj_local);
	            };

	            if (intval($requested_staff_id) === 0) {
	                $eligible_for_slot = yatoon_db_get_results($wpdb->prepare(
	                    "SELECT s.* FROM $table_staff s
	                     INNER JOIN $table_ss_local ss ON s.id = ss.staff_id
	                     WHERE ss.service_id = %d AND s.is_active = 1 AND s.is_bookable = 1
	                     ORDER BY s.sort_order ASC, s.name ASC",
	                    $service_id
	                ));
                    if (empty($eligible_for_slot)) {
                        $eligible_for_slot = yatoon_db_get_results(
                            "SELECT * FROM $table_staff WHERE is_active = 1 AND is_bookable = 1 ORDER BY sort_order ASC, name ASC"
                        );
                    }

	                $staff = null;
	                foreach ((array) $eligible_for_slot as $candidate_staff) {
	                    if ($local_slot_is_available(intval($candidate_staff->id))) {
	                        $staff = $candidate_staff;
	                        $staff_id = intval($candidate_staff->id);
	                        break;
	                    }
	                }
	            }

	            $slot_staff_id = !empty($staff->id) ? intval($staff->id) : $staff_id;
	            if (!$local_slot_is_available($slot_staff_id)) {
	                wp_send_json_error(array('message' => __('Sorry, that time slot is no longer available. Please choose another time.', 'yatoon-booking-system')));
	            }

            $booking_notes   = $notes;
            $booking_benefit = array();
            if (class_exists('YATOON_Growth_Modules')) {
                $booking_benefit = YATOON_Growth_Modules::apply_customer_benefits_to_total($total_price, $customer_email, $customer_phone);
                if (!empty($booking_benefit['type'])) {
                    $total_price   = floatval($booking_benefit['total']);
                    $booking_notes = trim($booking_notes . "\n" . $booking_benefit['note']);
                }
            }
            if ($claimed_coupon && $total_price > 0) {
                $coupon_discount = YATOON_Growth_Modules::compute_coupon_discount_for($claimed_coupon, $total_price);
                if ($coupon_discount > 0) {
                    $total_price   = max(0, round($total_price - $coupon_discount, 2));
                    $booking_notes = trim($booking_notes . "\n" . sprintf('Coupon %s applied: -$%s', $coupon_code, number_format($coupon_discount, 2)));
                }
            }

            $result = yatoon_db_insert($table_bookings, array_merge(array(
                'square_booking_id'  => null,
                'customer_name'      => $customer_name,
                'customer_email'     => $customer_email,
                'customer_phone'     => $customer_phone,
                'service_id'         => $service_id,
                'staff_id'           => $staff->id,
                'start_time'         => $start_time->format('Y-m-d H:i:s'),
                'end_time'           => $end_time->format('Y-m-d H:i:s'),
                'status'             => $initial_status,
                'notes'              => $booking_notes,
                'tip_amount'         => $tip_amount,
                'custom_field_values'=> $custom_field_values,
                'total_price'        => $total_price + $tip_amount,
                'square_synced'      => 0,
                'payment_status'     => 'unpaid',
                'is_any_staff'       => ($staff_id === 0) ? 1 : 0,
                'group_booking_id'   => $booking_group_id ? $booking_group_id : null,
            ), $consent_booking_fields));

            if ($result === false) {
                if (class_exists('YATOON_Growth_Modules')) {
                    if (!empty($booking_benefit['type'])) YATOON_Growth_Modules::release_customer_benefit($booking_benefit);
                    if ($claimed_coupon) YATOON_Growth_Modules::release_coupon($coupon_code);
                }
                wp_send_json_error(array('message' => __('Failed to create booking. Please try again.', 'yatoon-booking-system')));
            }

            $booking_id = $wpdb->insert_id;
            if (class_exists('YATOON_Growth_Modules') && !empty($booking_benefit['type'])) {
                YATOON_Growth_Modules::record_booking_benefit($booking_benefit, $booking_id);
            }

            $manage_token = Yatoon_Manage_Booking::create_token($booking_id);
            $booking_data = array_merge(array(
                'customer_name'  => $customer_name,
                'customer_email' => $customer_email,
                'customer_phone' => $customer_phone,
                'start_time'     => $start_time->format('Y-m-d H:i:s'),
                'end_time'       => $end_time->format('Y-m-d H:i:s'),
                'total_price'    => $total_price,
                'notes'          => $booking_notes,
                'manage_token'   => $manage_token,
                'booking_id'     => $booking_id,
                'manage_url'     => $manage_token ? Yatoon_Manage_Booking::get_manage_url($manage_token) : '',
                'calendar_url'   => YATOON_Calendar_Links::get_calendar_url($booking_id, $manage_token ?? ''),
            ), $consent_booking_fields);

            if ($coupon_code && class_exists('YATOON_Growth_Modules')) {
                YATOON_Growth_Modules::mark_coupon_used($coupon_code);
            }

            if (!empty($selected_options)) {
                foreach ($selected_options as $opt_id) {
                    yatoon_db_insert($table_booking_options, array('booking_id' => $booking_id, 'option_id' => $opt_id));
                }
            }

            if (get_option('yatoon_send_confirmation_email', '1') === '1') {
                $this->send_confirmation_email($booking_data, $service, $staff);
            }

            $this->notify_admin_new_booking($booking_data, $service, $staff);
            YATOON_Twilio::send_customer_confirmation($booking_data, $service, $staff);
            YATOON_Twilio::send_admin_alert($booking_data, $service, $staff);

            if (YATOON_Google_Calendar::is_enabled()) {
                $google_event_id = YATOON_Google_Calendar::add_event($booking_data, $service, $staff);
                if ($google_event_id) {
                    yatoon_db_update($table_bookings, array('google_event_id' => $google_event_id), array('id' => $booking_id));
                }
            }

            // Redeem gift card if used
            if ($gift_card_code && $gift_card_amount > 0) {
                $gc_table = $wpdb->prefix . 'yatoon_gift_cards';
                $tx_table = $wpdb->prefix . 'yatoon_gift_card_transactions';
                $gc = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $gc_table WHERE code = %s AND status = 'active' LIMIT 1", $gift_card_code));
                if ($gc) {
                    $redeem   = min(floatval($gc->balance), $gift_card_amount);
                    $new_bal  = max(0, floatval($gc->balance) - $redeem);
                    $new_stat = $new_bal <= 0 ? 'used' : 'active';
                    yatoon_db_update($gc_table, array('balance' => $new_bal, 'status' => $new_stat), array('id' => $gc->id));
                    yatoon_db_insert($tx_table, array(
                        'gift_card_id'  => $gc->id,
                        'booking_id'    => $booking_id,
                        'type'          => 'redeem',
                        'amount'        => $redeem,
                        'balance_after' => $new_bal,
                        'note'          => 'Booking #' . $booking_id,
                    ));
                }
            }

            do_action('yatoon_booking_created', array_merge($booking_data, array('booking_id' => $booking_id)));

            YATOON_PWA::send_push_to_admins(array(
                'title'      => 'New Booking',
                'body'       => $customer_name . ' - ' . $service->name . ' at ' . wp_date('g:i A', strtotime($booking_data['start_time'])),
                'url'        => admin_url('admin.php?page=yatoon-booking-system'),
                'booking_id' => $booking_id,
            ));

            wp_send_json_success(array(
                'message'      => __('Booking created successfully!', 'yatoon-booking-system'),
                'booking_id'   => $booking_id,
                'manage_url'   => $booking_data['manage_url'],
                'calendar_url' => $booking_data['calendar_url'],
                'service_name' => $service->name ?? '',
                'square_synced'=> false,
                'mode'         => 'local',
                'design_attachment_id' => $design_reference_fields['design_attachment_id'] ?? 0,
                'design_image_url' => $design_reference_fields['design_image_url'] ?? '',
                'design_upload_token' => $design_reference_fields['design_upload_token'] ?? '',
            ));
        }

        // Full Square API booking flow
        if ($platform !== 'square') {
            wp_send_json_error(array('message' => __('Booking could not be completed. Please refresh and try again.', 'yatoon-booking-system')));
        }
        if (!$this->square_api || !is_object($this->square_api)) {
            wp_send_json_error(array('message' => __('Square is not connected. Please switch to Local mode or reconnect Square.', 'yatoon-booking-system')));
        }
        // ------------------------------------------------------------
        // Get variation ID - use MAIN (non-addon) option for Square variation.
        // Add-on options (e.g. Custom Design) are NOT Square variations;
        // they are sent as separate appointment_segments with their own duration.
        $selected_variation_id       = null;
        $selected_variation_version  = null;
        $selected_variation_duration = intval($service->duration_minutes); // main service duration ONLY (no addons)
        $addon_names_for_note        = array(); // collect add-on names for seller_note

        if (!empty($selected_options)) {
            $table_options_sq  = $wpdb->prefix . 'yatoon_service_options';
            $sq_ids_str        = implode(',', array_map('intval', $selected_options));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $sq_all_opts       = yatoon_db_get_results(
                "SELECT * FROM $table_options_sq WHERE id IN ($sq_ids_str) ORDER BY CASE WHEN group_name = 'Add-on' THEN 1 ELSE 0 END, sort_order ASC, id ASC"
            );
            foreach ($sq_all_opts as $sq_opt) {
                $is_addon_sq = (isset($sq_opt->group_name) && $sq_opt->group_name === 'Add-on');
                if (!$is_addon_sq && $selected_variation_id === null) {
                    $selected_variation_id      = $sq_opt->square_id;
                    $selected_variation_version = $sq_opt->square_version ?? null;
                    // Use main option's own duration if set
                    if (intval($sq_opt->duration_minutes) > 0) {
                        $selected_variation_duration = intval($sq_opt->duration_minutes);
                    }
                } elseif ($is_addon_sq) {
                    $addon_names_for_note[] = $sq_opt->name;
                }
            }
        }
        if (!$selected_variation_id) {
            $selected_variation_id      = $service->square_id;
            $selected_variation_version = isset($service->square_version) ? $service->square_version : null;
        }

        // Square customer
        $customer_id     = null;
        $customer_search = $customer_email ? $this->square_api->search_customers($customer_email) : null;
        if (!is_wp_error($customer_search) && isset($customer_search['customers'][0])) {
            $customer_id = $customer_search['customers'][0]['id'];
        } else {
            $phone_search = $customer_phone ? $this->square_api->search_customers(null, $customer_phone) : null;
            if (!is_wp_error($phone_search) && isset($phone_search['customers'][0])) {
                $customer_id = $phone_search['customers'][0]['id'];
            }
        }
        if (!$customer_id) {
            $name_parts     = explode(' ', $customer_name, 2);
            $phone_digits   = preg_replace('/[^0-9]/', '', $customer_phone);
            $formatted_phone = strlen($phone_digits) === 10 ? '+1'.$phone_digits : '+'.$phone_digits;
            $customer_resp  = $this->square_api->create_customer(array(
                'given_name'  => $name_parts[0],
                'family_name' => $name_parts[1] ?? '',
                'email'       => $customer_email,
                'phone'       => $formatted_phone,
            ));
            if (!is_wp_error($customer_resp) && isset($customer_resp['customer']['id'])) {
                $customer_id = $customer_resp['customer']['id'];
            }
        }

        if (!$customer_id) {
            wp_send_json_error(array('message' => __('Failed to create customer in Square', 'yatoon-booking-system')));
        }

        // Get version (if missing)
        if (empty($selected_variation_version)) {
            $cat = $this->square_api->get_catalog_object($selected_variation_id);
            if (!is_wp_error($cat) && isset($cat['object']['version'])) {
                $selected_variation_version = $cat['object']['version'];
            }
        }

        $timezone        = ( wp_timezone_string() ?: 'America/Chicago' );
        $start_time_obj  = new DateTime($start_at);
        $start_time_obj->setTimezone(new DateTimeZone($timezone));
        $formatted_start = $start_time_obj->format('c');

        // -- Square multi-service booking: create one booking per service ----------
        // NOTE: Square Bookings API does NOT support start_at_minutes_offset.
        // The correct approach is to create a separate booking for each service,
        // each with its own correctly-computed start_at time.
        // --------------------------------------------------------------------------

        $services_json  = isset($_POST['services_json']) ? wp_unslash( $_POST['services_json'] ) : '';
        $multi_services = $services_json ? json_decode( $services_json, true ) : null;

        if ($multi_services && is_array($multi_services) && count($multi_services) > 1) {
            // -- Build eligible-staff helper --------------------------------------
            $table_options_tb = $wpdb->prefix . 'yatoon_service_options';
            $table_ss         = $wpdb->prefix . 'yatoon_staff_services';

            $get_eligible_for_service = function($svc_id) use ($wpdb, $table_staff, $table_ss) {
                $assigned = yatoon_db_get_results($wpdb->prepare(
                    "SELECT s.* FROM $table_staff s
                     INNER JOIN $table_ss ss ON s.id = ss.staff_id
                     WHERE ss.service_id = %d AND s.is_active = 1 AND s.is_bookable = 1
                     ORDER BY s.sort_order ASC, s.name ASC",
                    $svc_id
                ));
                if (!empty($assigned)) return $assigned;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                return yatoon_db_get_results(
                    "SELECT * FROM $table_staff WHERE is_active=1 AND is_bookable=1 ORDER BY sort_order ASC, name ASC"
                );
            };

            // -- Assign staff to each service -------------------------------------
            $assigned_staff_ids = array();
            $used_staff_sq_ids  = array();
            $is_any_staff_idx   = array(); // tracks which indices are "any staff" (staff_id = 0)

            // Pass 1: named staff
            $staff_map_json_initial = isset($_POST['staff_map_json']) ? wp_unslash($_POST['staff_map_json']) : '';
            $staff_map_initial = $staff_map_json_initial ? json_decode($staff_map_json_initial, true) : null;
            foreach ($multi_services as $idx => $svc_data) {
                $svc_stf = intval($svc_data['staff_id'] ?? 0);
                if ($svc_stf > 0) {
                    $stf_row = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_staff WHERE id = %d", $svc_stf));
                    if ($stf_row) {
                        $assigned_staff_ids[$idx] = $stf_row;
                        $used_staff_sq_ids[]      = $stf_row->square_id;
                    }
                } elseif (is_array($staff_map_initial) && !empty($staff_map_initial[$idx])) {
                    $mapped_square_staff_id = sanitize_text_field($staff_map_initial[$idx]);
                    $stf_row = yatoon_db_get_row($wpdb->prepare(
                        "SELECT * FROM $table_staff WHERE square_id = %s AND is_active = 1 AND is_bookable = 1 LIMIT 1",
                        $mapped_square_staff_id
                    ));
                    if ($stf_row) {
                        $assigned_staff_ids[$idx] = $stf_row;
                        $is_any_staff_idx[$idx]   = true;
                        $used_staff_sq_ids[]      = $stf_row->square_id;
                    }
                }
            }
            // Pass 2: "Any staff" - assign staff using Square real-time availability API.
            //
            // Strategy: for each any-staff position, query Square's search_availability
            // with the EXACT time window for that service (accounting for sequential offsets).
            // Square has real-time data - no DB end_time stale issue.
            //
            // Window trick: Square requires >=61min window, so we query
            // [svc_start - 1min, svc_start + 61min] and check if svc_start appears.
            {
                $staff_map_json    = isset($_POST['staff_map_json']) ? $_POST['staff_map_json'] : '';
                $staff_map_from_sq = $staff_map_json ? json_decode($staff_map_json, true) : null;
                $timezone_p2       = ( wp_timezone_string() ?: 'America/Chicago' );
                $tz_obj_p2         = new DateTimeZone($timezone_p2);

                // Pre-compute cumulative offsets per person. In Square group bookings,
                // different guests must start together; only one person's own
                // services are sequential.
                $pos_durations_p2  = array();
                $guest_offsets_p2   = array();
                foreach ($multi_services as $idx => $svc_data) {
                    $p_guest_label = isset($svc_data['guest_label']) && $svc_data['guest_label'] !== ''
                        ? sanitize_text_field($svc_data['guest_label'])
                        : 'Me';
                    $p_svc_id = intval($svc_data['service_id'] ?? 0);
                    $p_opts   = array_map('intval', (array)($svc_data['option_ids'] ?? []));
                    $p_dur    = 0;
                    $p_main_dur = 0;
                    $p_addon_dur = 0;
                    if (!empty($p_opts)) {
                        $p_ids = implode(',', array_map('intval', $p_opts));
                        $p_rows = yatoon_db_get_results(
                            "SELECT duration_minutes, group_name FROM {$table_options_tb} WHERE id IN ($p_ids)"
                        );
                        foreach ($p_rows as $p_row) {
                            if ((string) $p_row->group_name === 'Add-on') {
                                $p_addon_dur += max(0, intval($p_row->duration_minutes));
                            } elseif ($p_main_dur === 0 && intval($p_row->duration_minutes) > 0) {
                                $p_main_dur = intval($p_row->duration_minutes);
                            }
                        }
                        $p_dur = $p_main_dur + $p_addon_dur;
                    }
                    if (!$p_dur && $p_svc_id) {
                        $d = yatoon_db_get_var($wpdb->prepare(
                            "SELECT duration_minutes FROM $table_services WHERE id = %d", $p_svc_id
                        ));
                        if ($d) $p_dur = intval($d);
                    }
                    if (!isset($guest_offsets_p2[$p_guest_label])) {
                        $guest_offsets_p2[$p_guest_label] = 0;
                    }
                    $pos_durations_p2[$idx] = array(
                        'duration'    => $p_dur ?: 60,
                        'offset'      => $guest_offsets_p2[$p_guest_label],
                        'guest_label' => $p_guest_label,
                    );
                    $guest_offsets_p2[$p_guest_label] += ($p_dur ?: 60);
                }

                foreach ($multi_services as $idx => $svc_data) {
                    if (isset($assigned_staff_ids[$idx])) continue; // already named

                    $svc_id   = intval($svc_data['service_id'] ?? 0);
                    $svc_opts = array_map('intval', (array)($svc_data['option_ids'] ?? []));
                    $offset   = $pos_durations_p2[$idx]['offset'] ?? 0;
                    $dur      = $pos_durations_p2[$idx]['duration'] ?? 60;

                    // Compute exact start time for this service
                    $svc_start = clone $start_time_obj;
                    if ($offset > 0) $svc_start->modify("+{$offset} minutes");

                    // Square requires >=61min window.
                    // Start window exactly at svc_start so Square checks if staff can BEGIN
                    // a service of $dur minutes at this time (respects their next appointment).
                    // End = svc_start + dur + 61min (satisfies Square minimum window).
                    $sq_win_s = clone $svc_start;
                    $sq_win_e = clone $svc_start;
                    $sq_win_e->modify('+' . ($dur + 61) . ' minutes');
                    $sq_win_s->setTimezone(new DateTimeZone('UTC'));
                    $sq_win_e->setTimezone(new DateTimeZone('UTC'));
                    $sq_s_iso = $sq_win_s->format('Y-m-d\TH:i:s\Z');
                    $sq_e_iso = $sq_win_e->format('Y-m-d\TH:i:s\Z');

                    // Resolve variation ID for this service
                    $var_id = null;
                    if (!empty($svc_opts)) {
                        $v = yatoon_db_get_var($wpdb->prepare(
                            "SELECT square_id FROM {$table_options_tb} WHERE id = %d AND square_id != '' LIMIT 1",
                            $svc_opts[0]
                        ));
                        if ($v) $var_id = $v;
                    }
                    if (!$var_id && $svc_id) {
                        $v = yatoon_db_get_var($wpdb->prepare(
                            "SELECT square_id FROM {$table_options_tb} WHERE service_id = %d AND square_id != '' LIMIT 1",
                            $svc_id
                        ));
                        if ($v) $var_id = $v;
                    }

                    $target_time_key = $svc_start->setTimezone($tz_obj_p2)->format('H:i');
                    $chosen = null;

                    // Get eligible staff for this service
                    $eligible = $get_eligible_for_service($svc_id);

                    // Build candidate list: prefer unused staff first, then already-used
                    $not_used     = array();
                    $already_used = array();
                    // Also exclude staff already assigned in THIS booking at overlapping times
                    foreach ($eligible as $cand) {
                        $skip = false;
                        foreach ($assigned_staff_ids as $prev_idx => $prev_stf) {
                            if (!$prev_stf || $prev_stf->id != $cand->id) continue;
                            // Check if time windows overlap
                            $prev_off = $pos_durations_p2[$prev_idx]['offset'] ?? 0;
                            $prev_dur = $pos_durations_p2[$prev_idx]['duration'] ?? 60;
                            $prev_end_off = $prev_off + $prev_dur;
                            $cur_end_off  = $offset + $dur;
                            if ($prev_off < $cur_end_off && $prev_end_off > $offset) {
                                $skip = true; break; // overlapping window - can't use same staff
                            }
                        }
                        if ($skip) continue;
                        if (in_array($cand->square_id, $used_staff_sq_ids, true)) {
                            $already_used[] = $cand;
                        } else {
                            $not_used[] = $cand;
                        }
                    }

                    // Step 1: Query Square without specifying a team member.
                    // Square returns ALL truly available staff for this time, respecting
                    // working hours AND personal events (Busy / All-Day blocks).
                    // This gives us a whitelist - only staff on this list can be assigned.
                    $svc_start_ts = $svc_start->getTimestamp();
                    $p2_any_resp  = $var_id ? $this->square_api->search_availability($var_id, $sq_s_iso, $sq_e_iso) : null;
                    $p2_available_ids = array();
                    if ($p2_any_resp && !is_wp_error($p2_any_resp) && !empty($p2_any_resp['availabilities'])) {
                        foreach ($p2_any_resp['availabilities'] as $p2_av) {
                            if (empty($p2_av['start_at'])) continue;
                            $p2_av_ts = (new DateTime($p2_av['start_at']))->getTimestamp();
                            if (abs($p2_av_ts - $svc_start_ts) <= 15 * 60) {
                                foreach ($p2_av['appointment_segments'] ?? [] as $p2_seg) {
                                    $tid = $p2_seg['team_member_id'] ?? '';
                                    if ($tid) $p2_available_ids[$tid] = true;
                                }
                            }
                        }
                    }
                    // Log raw availabilities to see actual Square response structure
                    if (!empty($p2_any_resp['availabilities'])) {
                        foreach (array_slice($p2_any_resp['availabilities'], 0, 3) as $dbg_av) {
                            yatoon_safe_log("SBS Pass2 RAW av: start=" . ($dbg_av['start_at'] ?? '?') . " segments=" . json_encode($dbg_av['appointment_segments'] ?? 'MISSING'));
                        }
                    }
                    yatoon_safe_log("SBS Pass2: idx=$idx Square available IDs at {$target_time_key}: " . implode(', ', array_keys($p2_available_ids)));

                    // Step 2: For each candidate (already_used first), ask Square directly
                    // whether they are available - AND confirm they appear in the whitelist above.
                    // The whitelist check filters out All-Day Blocked staff that Square's
                    // per-staff query may incorrectly return as available.
                    foreach (array_merge($already_used, $not_used) as $candidate) {
                        if (empty($candidate->square_id) || !$var_id) continue;

                        // Must be in the whitelist (Square any-staff query)
                        if (!empty($p2_available_ids) && !isset($p2_available_ids[$candidate->square_id])) {
                            yatoon_safe_log("SBS Pass2: idx=$idx {$candidate->name} - blocked (not in Square available list)");
                            continue;
                        }

                        // Ask Square directly for this staff member
                        $resp = $this->square_api->search_availability($var_id, $sq_s_iso, $sq_e_iso, $candidate->square_id);
                        if (is_wp_error($resp) || empty($resp['availabilities'])) {
                            yatoon_safe_log("SBS Pass2: idx=$idx {$candidate->name} - Square says not available");
                            continue;
                        }
                        foreach ($resp['availabilities'] as $av) {
                            if (empty($av['start_at'])) continue;
                            $av_ts = (new DateTime($av['start_at']))->getTimestamp();
                            if (abs($av_ts - $svc_start_ts) <= 15 * 60) {
                                $chosen = $candidate;
                                yatoon_safe_log("SBS Pass2: idx=$idx -> {$chosen->name} (Square confirmed at {$target_time_key})");
                                break 2;
                            }
                        }
                        yatoon_safe_log("SBS Pass2: idx=$idx {$candidate->name} - no slot near {$target_time_key}");
                    }

                    if (!$chosen) {
                        wp_send_json_error(array(
                            'message'  => __('No staff available for one of the services. Please choose a different time.', 'yatoon-booking-system'),
                            'no_staff' => true,
                        ));
                        return;
                    }

                    $assigned_staff_ids[$idx] = $chosen;
                    $is_any_staff_idx[$idx]   = true;
                    if (!in_array($chosen->square_id, $used_staff_sq_ids, true)) {
                        $used_staff_sq_ids[] = $chosen->square_id;
                    }
                }
            }

            // Phase 1: Collect all segments + local metadata (no API calls yet)
            $segments_to_create = array(); // each entry: [seg, svc_row, stf_row, svc_start, svc_end, svc_price, svc_opts, svc_id, is_any_staff]
            $guest_cumulative_min = array();
            $growth_reserved_packages = array();

            foreach ($multi_services as $idx => $svc_data) {
                $guest_label = isset($svc_data['guest_label']) && $svc_data['guest_label'] !== ''
                    ? sanitize_text_field($svc_data['guest_label'])
                    : 'Me';
                $svc_id   = intval($svc_data['service_id'] ?? 0);
                $svc_opts = array_map('intval', (array)($svc_data['option_ids'] ?? []));

                $svc_row = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table_services WHERE id = %d", $svc_id));
                if (!$svc_row) continue;

                $stf_row = $assigned_staff_ids[$idx] ?? null;
                if (!$stf_row) {
                    $fallback = $get_eligible_for_service($svc_id);
                    $stf_row  = !empty($fallback) ? $fallback[0] : null;
                }
                if (!$stf_row) continue;

                // Resolve variation ID and duration
                // Separate Main option (has Square variation, single-select) from
                // Add-on options (multi-select, duration added on top of main).
                $var_id      = null;
                $var_dur     = intval($svc_row->duration_minutes);
                $var_ver     = null;
                $addon_extra = 0; // total extra minutes from Add-on options
                $addon_segments = array();

                if (!empty($svc_opts)) {
                    $ids_pl = implode(',', array_map('intval', $svc_opts));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $all_opts = yatoon_db_get_results(
                        "SELECT * FROM $table_options_tb WHERE id IN ($ids_pl) ORDER BY CASE WHEN group_name = 'Add-on' THEN 1 ELSE 0 END, sort_order ASC, id ASC"
                    );
                    // Main option = group_name IS NULL or '' (single-select)
                    // Add-on option = group_name = 'Add-on'
                    $main_opt = null;
                    foreach ($all_opts as $o) {
                        $is_addon = (isset($o->group_name) && $o->group_name === 'Add-on');
                        if (!$is_addon && !$main_opt) {
                            $main_opt = $o; // first non-addon = main
                        }
                        if ($is_addon) {
                            $addon_extra += intval($o->duration_minutes); // add addon duration
                            // Multi-service bookings used to omit add-ons entirely from
                            // Square. Preserve each catalog-backed add-on as its own segment,
                            // just like the single-service booking path does.
                            if (!empty($o->square_id)) {
                                $addon_ver = !empty($o->square_version) ? intval($o->square_version) : null;
                                if (!$addon_ver) {
                                    $addon_cat = $this->square_api->get_catalog_object($o->square_id);
                                    if (!is_wp_error($addon_cat) && isset($addon_cat['object']['version'])) {
                                        $addon_ver = intval($addon_cat['object']['version']);
                                        yatoon_db_update($table_options_tb, array('square_version' => $addon_ver), array('id' => $o->id));
                                    }
                                }
                                if ($addon_ver) {
                                    $addon_segments[] = array(
                                        'duration_minutes'          => max(1, intval($o->duration_minutes)),
                                        'service_variation_id'      => $o->square_id,
                                        'service_variation_version' => $addon_ver,
                                        'team_member_id'            => $stf_row->square_id,
                                    );
                                } else {
                                    yatoon_safe_log("Yatoon: Add-on {$o->id} ({$o->name}) has no Square catalog version; it cannot be sent as a segment.");
                                }
                            }
                        }
                    }
                    if ($main_opt) {
                        $var_id  = $main_opt->square_id;
                        $var_ver = $main_opt->square_version ?? null;
                        if (intval($main_opt->duration_minutes) > 0) {
                            $var_dur = intval($main_opt->duration_minutes);
                        }
                    }
                }
                $var_dur_sq = $var_dur;              // Square segment duration (main only)
                $var_dur   += $addon_extra;            // local total = main + addons
                if (!$var_id) {
                    $opt = yatoon_db_get_row($wpdb->prepare(
                        "SELECT square_id, square_version FROM $table_options_tb WHERE service_id=%d AND square_id!='' LIMIT 1", $svc_id
                    ));
                    if ($opt) { $var_id = $opt->square_id; $var_ver = $opt->square_version ?? null; }
                }
                if (!$var_id && !empty($svc_row->square_id)) {
                    $var_id  = $svc_row->square_id;
                    $var_ver = $svc_row->square_version ?? null;
                }
                if (!$var_id) {
                    yatoon_safe_log("Yatoon: No Square variation ID for service {$svc_id} ({$svc_row->name})");
                    if (!isset($guest_cumulative_min[$guest_label])) {
                        $guest_cumulative_min[$guest_label] = 0;
                    }
                    $guest_cumulative_min[$guest_label] += $var_dur;
                    continue;
                }

                // Get version if missing
                if (empty($var_ver)) {
                    $cat = $this->square_api->get_catalog_object($var_id);
                    if (!is_wp_error($cat) && isset($cat['object']['version'])) {
                        $var_ver = intval($cat['object']['version']);
                    }
                }

                // Calculate this service's start/end time
                if (!isset($guest_cumulative_min[$guest_label])) {
                    $guest_cumulative_min[$guest_label] = 0;
                }
                $svc_start = clone $start_time_obj;
                if ($guest_cumulative_min[$guest_label] > 0) {
                    $svc_start->modify("+{$guest_cumulative_min[$guest_label]} minutes");
                }
                $svc_end = clone $svc_start;
                $svc_end->modify("+{$var_dur} minutes"); // includes addon time

                // Build segment - Square uses the variation's own duration (no addon padding)
                $seg = array(
                    'duration_minutes'     => $var_dur_sq,
                    'service_variation_id' => $var_id,
                    'team_member_id'       => $stf_row->square_id,
                );
                if ($var_ver) $seg['service_variation_version'] = intval($var_ver);
                $square_segments = array_merge(array($seg), $addon_segments);

                // Resolve price: sum main option modifier + all addon modifiers
                $svc_price = floatval($svc_row->price);
                if (!empty($svc_opts)) {
                    $ids_pl = implode(',', array_map('intval', $svc_opts));
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $price_opts = yatoon_db_get_results(
                        "SELECT price_modifier FROM $table_options_tb WHERE id IN ($ids_pl)"
                    );
                    foreach ($price_opts as $po) $svc_price += floatval($po->price_modifier);
                }

                $booking_notes = $notes;
                $booking_benefit = array();
                if (class_exists('YATOON_Growth_Modules')) {
                    $booking_benefit = YATOON_Growth_Modules::apply_customer_benefits_to_total($svc_price, $customer_email, $customer_phone, $growth_reserved_packages);
                    if (!empty($booking_benefit['type'])) {
                        $svc_price = floatval($booking_benefit['total']);
                        $booking_notes = trim($booking_notes . "\n" . $booking_benefit['note']);
                    }
                }
                if ($claimed_coupon && $svc_price > 0) {
                    $is_fixed_coupon = ($claimed_coupon->discount_type === 'fixed');
                    if (!$is_fixed_coupon || intval($idx) === 0) {
                        $coupon_discount = YATOON_Growth_Modules::compute_coupon_discount_for($claimed_coupon, $svc_price);
                        if ($coupon_discount > 0) {
                            $svc_price = max(0, round($svc_price - $coupon_discount, 2));
                            $booking_notes = trim($booking_notes . "\n" . sprintf('Coupon %s applied: -$%s', $coupon_code, number_format($coupon_discount, 2)));
                        }
                    }
                }
                $is_any_staff = isset($is_any_staff_idx[$idx]) ? true : false;
                $segments_to_create[] = compact('seg', 'square_segments', 'svc_row', 'stf_row', 'svc_start', 'svc_end', 'svc_price', 'svc_opts', 'svc_id', 'is_any_staff', 'booking_notes', 'booking_benefit', 'guest_label');
                $guest_cumulative_min[$guest_label] += $var_dur;
            }

            if (empty($segments_to_create)) {
                wp_send_json_error(array('message' => __('Failed to resolve services. Please ensure services are synced with Square.', 'yatoon-booking-system')));
            }

            // BUG FIX (v2.92): DB overlap check for all assigned staff
            // Runs AFTER staff assignment (incl. any-staff) and BEFORE Square API call.
            // Prevents two concurrent bookings (e.g. couple both choosing "any staff"
            // at 3 PM Sunday) from landing on the same staff member.
            {
                $table_bk_ov = $wpdb->prefix . 'yatoon_bookings';
                foreach ($segments_to_create as $entry_ov) {
                    $ov_staff_id = isset($entry_ov['stf_row']->id) ? intval($entry_ov['stf_row']->id) : 0;
                    if (!$ov_staff_id) continue;
                    $ov_start = $entry_ov['svc_start']->format('Y-m-d H:i:s');
                    $ov_end   = $entry_ov['svc_end']->format('Y-m-d H:i:s');
                    $conflict_id = yatoon_db_get_var($wpdb->prepare(
                        "SELECT id FROM $table_bk_ov
                         WHERE staff_id = %d
	                           AND status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')
                           AND start_time < %s
                           AND end_time   > %s
                         LIMIT 1",
                        $ov_staff_id, $ov_end, $ov_start
                    ));
                    if ($conflict_id) {
                        // DB shows conflict but DB may be stale - log it and let Square decide.
                        // (e.g. customer cancelled in Square but DB not yet synced)
                        $stf_nm = $entry_ov['stf_row']->name ?? 'A staff member';
                        yatoon_safe_log("SBS overlap-check: DB conflict for staff {$stf_nm} at {$ov_start}-{$ov_end} (booking id={$conflict_id}), but proceeding - Square is the final arbiter.");
                    }
                }
            }
            // end overlap check

            // Phase 2: Create one Square booking per person. Square does not support
            // start_at offsets, and Me/Guest appointments must remain separate so
            // they can begin at the same time with different staff.
            $segments_by_guest = array();
            foreach ($segments_to_create as $entry) {
                $entry_guest_label = isset($entry['guest_label']) && $entry['guest_label'] !== '' ? $entry['guest_label'] : 'Me';
                if (!isset($segments_by_guest[$entry_guest_label])) {
                    $segments_by_guest[$entry_guest_label] = array();
                }
                $segments_by_guest[$entry_guest_label][] = $entry;
            }

            $created_square_booking_ids = array();
            $group_booking_id = $booking_group_id ? $booking_group_id : wp_generate_uuid4();
            foreach ($segments_by_guest as $booking_guest_label => $guest_entries) {
                $all_segs = array();
                foreach ($guest_entries as $guest_entry) {
                    $entry_segments = !empty($guest_entry['square_segments']) ? $guest_entry['square_segments'] : array($guest_entry['seg']);
                    $all_segs = array_merge($all_segs, $entry_segments);
                }
                $sq_payload = array(
                    'location_id'          => get_option('yatoon_square_location_id'),
                    'customer_id'          => $customer_id,
                    'start_at'             => $guest_entries[0]['svc_start']->format('c'),
                    'appointment_segments' => $all_segs,
                );
                if ($notes) $sq_payload['customer_note'] = $notes;
                // Build seller note - include guest and "any available" info.
                $any_staff_service_names = array();
                foreach ($guest_entries as $entry) {
                    if (!empty($entry['is_any_staff'])) {
                        $any_staff_service_names[] = $entry['svc_row']->name;
                    }
                }
                $seller_note = 'Online booking by ' . $customer_name;
                if ($booking_guest_label !== 'Me') {
                    $seller_note .= '. Guest: ' . $booking_guest_label;
                }
                if (!empty($any_staff_service_names)) {
                    $seller_note .= '. Booked with any available staff: ' . implode(', ', $any_staff_service_names);
                }
                $sq_payload['seller_note'] = $seller_note;

                $sq_resp = $this->square_api->create_booking($sq_payload);
                if (is_wp_error($sq_resp) || !isset($sq_resp['booking']['id'])) {
                    foreach ($created_square_booking_ids as $created_square_booking_id) {
                        $this->square_api->cancel_booking($created_square_booking_id);
                    }
                    // Roll back EVERY package visit / coupon claim we reserved
                    // earlier in this multi-service loop, otherwise the customer
                    // permanently loses balance for a booking that never existed.
                    if (class_exists('YATOON_Growth_Modules')) {
                        foreach ($segments_to_create as $rb_entry) {
                            if (!empty($rb_entry['booking_benefit']['type'])) {
                                YATOON_Growth_Modules::release_customer_benefit($rb_entry['booking_benefit']);
                            }
                        }
                        if ($claimed_coupon) YATOON_Growth_Modules::release_coupon($coupon_code);
                    }
                    if (is_wp_error($sq_resp)) {
                        wp_send_json_error(array('message' => __('Failed to create booking in Square: ', 'yatoon-booking-system') . $sq_resp->get_error_message()));
                    }
                    $sq_err = isset($sq_resp['errors']) ? implode(' ', array_column($sq_resp['errors'], 'detail')) : '';
                    wp_send_json_error(array('message' => __('Booking not confirmed by Square. ', 'yatoon-booking-system') . $sq_err));
                }
            $sq_booking_id    = $sq_resp['booking']['id'];
                $created_square_booking_ids[] = $sq_booking_id;

                // Phase 3: Save each service segment to local DB. Segments for the
                // same person share a Square booking ID; other guests get their own.
                foreach ($guest_entries as $entry) {
                $row = array_merge( array(
                    'square_booking_id' => $sq_booking_id,
                    'customer_name'     => $customer_name,
                    'customer_email'    => $customer_email,
                    'customer_phone'    => $customer_phone,
                    'service_id'        => $entry['svc_id'],
                    'staff_id'          => $entry['stf_row']->id,
                    'start_time'        => $entry['svc_start']->format('Y-m-d H:i:s'),
                    'end_time'          => $entry['svc_end']->format('Y-m-d H:i:s'),
                    'status'            => $initial_status,
                    'notes'             => $entry['booking_notes'] ?? $notes,
                    'total_price'       => $entry['svc_price'],
                    'square_synced'     => 1,
                    'payment_status'    => 'unpaid',
                    'is_any_staff'      => isset($entry['is_any_staff']) ? 1 : 0,
                    'group_booking_id'  => $group_booking_id,
                    'service_order'     => $service_order_idx,
                ), $consent_booking_fields );
                yatoon_db_insert($table_bookings, $row);
                $local_id = $wpdb->insert_id;
                if (class_exists('YATOON_Growth_Modules') && !empty($entry['booking_benefit']['type'])) {
                    YATOON_Growth_Modules::record_booking_benefit($entry['booking_benefit'], $local_id);
                }
                if ($first_booking_id === null) $first_booking_id = $local_id;
                $service_order_idx++;

                if (!empty($entry['svc_opts'])) {
                    foreach ($entry['svc_opts'] as $opt_id) {
                        yatoon_db_insert($table_booking_options, array('booking_id' => $local_id, 'option_id' => $opt_id));
                    }
                }

                $all_services_info[] = array(
                    'service'    => $entry['svc_row'],
                    'staff'      => $entry['stf_row'],
                    'start_time' => $entry['svc_start']->format('Y-m-d H:i:s'),
                    'end_time'   => $entry['svc_end']->format('Y-m-d H:i:s'),
                    'price'      => $entry['svc_price'],
                );
                }
                // Square can normalize catalog durations. Re-read the returned
                // segment sequence immediately so local rows match Square from
                // the moment the booking is created, not only after cron runs.
                if (!empty($sq_resp['booking'])) {
                    yatoon_reconcile_square_booking_segments($sq_resp['booking']);
                }
            }

            if (empty($all_services_info)) {
                wp_send_json_error(array('message' => __('Failed to create any bookings. Please ensure services are synced with Square.', 'yatoon-booking-system')));
            }

            // Send confirmation email for multi-service
            $booking_data_multi = array(
                'customer_name'  => $customer_name,
                'customer_email' => $customer_email,
                'customer_phone' => $customer_phone,
                'start_time'     => $all_services_info[0]['start_time'],
                'end_time'       => end($all_services_info)['end_time'],
                'total_price'    => array_sum(array_column($all_services_info, 'price')),
                'notes'          => $notes,
                'services_info'  => $all_services_info,
                'manage_token'   => $first_booking_id ? Yatoon_Manage_Booking::create_token( $first_booking_id ) : '',
                'booking_id'     => $first_booking_id,
            ) + $consent_booking_fields;
            $booking_data_multi['manage_url'] = ! empty( $booking_data_multi['manage_token'] )
                ? Yatoon_Manage_Booking::get_manage_url( $booking_data_multi['manage_token'] )
                : '';
            $booking_data_multi['calendar_url'] = YATOON_Calendar_Links::get_calendar_url( $first_booking_id, $booking_data_multi['manage_token'] ?? '' );
            if ($coupon_code && class_exists('YATOON_Growth_Modules')) {
                YATOON_Growth_Modules::mark_coupon_used($coupon_code);
            }
            // Build combined service name for multi-service email
            $combined_service_names_sq = array_map(function($si) { return $si['service']->name; }, $all_services_info);
            $combined_service_obj_sq = clone $all_services_info[0]['service'];
            $combined_service_obj_sq->name = implode(', ', $combined_service_names_sq);

            if (get_option('yatoon_send_confirmation_email', '1') === '1') {
                $this->send_confirmation_email($booking_data_multi, $combined_service_obj_sq, $all_services_info[0]['staff']);
            }
            $this->notify_admin_new_booking($booking_data_multi, $combined_service_obj_sq, $all_services_info[0]['staff']);
            YATOON_Twilio::send_customer_confirmation($booking_data_multi, $combined_service_obj_sq, $all_services_info[0]['staff']);
            YATOON_Twilio::send_admin_alert($booking_data_multi, $combined_service_obj_sq, $all_services_info[0]['staff']);

            // Schedule reminder + review request emails
            do_action( 'yatoon_booking_created', array_merge( $booking_data_multi, array( 'booking_id' => $first_booking_id ) ) );

            wp_send_json_success(array(
                'message'       => __('Booking created successfully!', 'yatoon-booking-system'),
                'booking_id'    => $first_booking_id,
                'manage_url'    => $booking_data_multi['manage_url'],
                'calendar_url'  => $booking_data_multi['calendar_url'],
                'service_name'  => $combined_service_obj_sq->name ?? '',
                'square_synced' => true,
                'mode'          => 'square',
                'design_attachment_id' => $design_reference_fields['design_attachment_id'] ?? 0,
                'design_image_url' => $design_reference_fields['design_image_url'] ?? '',
                'design_upload_token' => $design_reference_fields['design_upload_token'] ?? '',
            ));
        }

        // -- Single-service Square booking (original logic) -----------------------
        // Main service segment
        $seg = array(
            'duration_minutes'     => intval($selected_variation_duration),
            'service_variation_id' => $selected_variation_id,
            'team_member_id'       => $staff->square_id,
        );
        if ($selected_variation_version) {
            $seg['service_variation_version'] = intval($selected_variation_version);
        }

        // Build all appointment segments: main + any Add-on options that have a Square variation ID
        $all_single_segs = array($seg);
        if (!empty($selected_options)) {
            $table_options_addon = $wpdb->prefix . 'yatoon_service_options';
            $addon_ids_str = implode(',', array_map('intval', $selected_options));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $addon_opts = yatoon_db_get_results(
                "SELECT * FROM $table_options_addon WHERE id IN ($addon_ids_str) ORDER BY sort_order ASC, id ASC"
            );
            foreach ($addon_opts as $addon_opt) {
                $is_addon_seg = (isset($addon_opt->group_name) && $addon_opt->group_name === 'Add-on');
                if ($is_addon_seg && !empty($addon_opt->square_id)) {
                    // Get version - required by Square API
                    $addon_ver = !empty($addon_opt->square_version) ? intval($addon_opt->square_version) : null;
                    if (empty($addon_ver)) {
                        $cat = $this->square_api->get_catalog_object($addon_opt->square_id);
                        if (!is_wp_error($cat) && isset($cat['object']['version'])) {
                            $addon_ver = intval($cat['object']['version']);
                            // Cache it back to DB so we don't need to fetch next time
                            yatoon_db_update(
                                $wpdb->prefix . 'yatoon_service_options',
                                array('square_version' => $addon_ver),
                                array('id' => $addon_opt->id)
                            );
                        }
                    }
                    if (empty($addon_ver)) {
                        yatoon_safe_log("Yatoon: Could not get square_version for Add-on option {$addon_opt->id} ({$addon_opt->name}), skipping segment.");
                        continue;
                    }
                    // Add-on has a Square variation - add as a separate segment with same staff
                    $addon_seg = array(
                        'duration_minutes'          => intval($addon_opt->duration_minutes) ?: 15,
                        'service_variation_id'      => $addon_opt->square_id,
                        'service_variation_version' => $addon_ver,
                        'team_member_id'            => $staff->square_id,
                    );
                    $all_single_segs[] = $addon_seg;
                }
            }
        }

        $booking_payload = array(
            'location_id'          => get_option('yatoon_square_location_id'),
            'customer_id'          => $customer_id,
            'start_at'             => $formatted_start,
            'appointment_segments' => $all_single_segs,
        );
        if ($notes) $booking_payload['customer_note'] = $notes;
        // Add seller_note - flag if staff was auto-assigned (any available)
        $single_seller_note = 'Online booking by ' . $customer_name;
        if (!empty($addon_names_for_note)) {
            $single_seller_note .= '. Add-ons: ' . implode(', ', $addon_names_for_note);
        }
        if ($staff_id === 0) {
            $single_seller_note .= '. Booked with any available staff: ' . $service->name;
        }
        $booking_payload['seller_note'] = $single_seller_note;

        $square_response   = $this->square_api->create_booking($booking_payload);
        $square_booking_id = null;

        if (is_wp_error($square_response)) {
            wp_send_json_error(array('message' => __('Failed to create booking in Square: ', 'yatoon-booking-system') . $square_response->get_error_message()));
        }
        if (isset($square_response['booking']['id'])) {
            $square_booking_id = $square_response['booking']['id'];
        } else {
            $sq_err = '';
            if (isset($square_response['errors'])) {
                $sq_err = ' ' . implode(' ', array_column($square_response['errors'], 'detail'));
            }
            wp_send_json_error(array('message' => __('Booking created but no confirmation ID returned', 'yatoon-booking-system') . $sq_err));
        }

        $booking_notes = $notes;
        $booking_benefit = array();
        if (class_exists('YATOON_Growth_Modules')) {
            $booking_benefit = YATOON_Growth_Modules::apply_customer_benefits_to_total($total_price, $customer_email, $customer_phone);
            if (!empty($booking_benefit['type'])) {
                $total_price = floatval($booking_benefit['total']);
                $booking_notes = trim($booking_notes . "\n" . $booking_benefit['note']);
            }
        }
        if ($claimed_coupon && $total_price > 0) {
            $coupon_result = array(
                'valid'    => true,
                'discount' => YATOON_Growth_Modules::compute_coupon_discount_for($claimed_coupon, $total_price),
            );
            if ($coupon_result['discount'] > 0) {
                $total_price = max(0, round($total_price - floatval($coupon_result['discount']), 2));
                $booking_notes = trim($booking_notes . "\n" . sprintf('Coupon %s applied: -$%s', $coupon_code, number_format($coupon_result['discount'], 2)));
            }
        }

        // Save locally
        $booking_data = array_merge( array(
            'square_booking_id' => $square_booking_id,
            'customer_name'     => $customer_name,
            'customer_email'    => $customer_email,
            'customer_phone'    => $customer_phone,
            'service_id'        => $service_id,
            'staff_id'          => $staff->id,
            'start_time'        => $start_time->format('Y-m-d H:i:s'),
            'end_time'          => $end_time->format('Y-m-d H:i:s'),
            'status'            => $initial_status,
            'notes'             => $booking_notes,
            'total_price'       => $total_price,
            'square_synced'     => 1,
            'payment_status'    => 'unpaid',
            'is_any_staff'      => ($staff_id === 0) ? 1 : 0,
            'group_booking_id'  => $booking_group_id ? $booking_group_id : null,
        ), $consent_booking_fields );
        $result = yatoon_db_insert($table_bookings, $booking_data);
        if ($result === false) {
            // Log the real MySQL error so this can actually be diagnosed next
            // time, instead of just "Failed to save booking" with no trace.
            if (function_exists('yatoon_safe_log')) {
                yatoon_safe_log( 'Booking insert failed: ' . $wpdb->last_error . ' | data: ' . wp_json_encode( $booking_data ) );
            }
            // Square booking already created but local insert failed: roll back
            // the local benefit + coupon reservations so the customer balance
            // is not silently consumed. (The Square booking remains and admin
            // will see it on next sync; this is the lesser of two evils.)
            if (class_exists('YATOON_Growth_Modules')) {
                if (!empty($booking_benefit['type'])) YATOON_Growth_Modules::release_customer_benefit($booking_benefit);
                if ($claimed_coupon) YATOON_Growth_Modules::release_coupon($coupon_code);
            }
            wp_send_json_error(array('message' => __('Failed to save booking', 'yatoon-booking-system')));
        }
        $booking_id = $wpdb->insert_id;
        if (class_exists('YATOON_Growth_Modules') && !empty($booking_benefit['type'])) {
            YATOON_Growth_Modules::record_booking_benefit($booking_benefit, $booking_id);
        }
        if ($coupon_code && class_exists('YATOON_Growth_Modules')) {
            YATOON_Growth_Modules::mark_coupon_used($coupon_code);
        }

        // Generate manage token for customer self-service
        $booking_data['manage_token'] = Yatoon_Manage_Booking::create_token( $booking_id );
        $booking_data['booking_id'] = $booking_id;
        $booking_data['manage_url'] = ! empty( $booking_data['manage_token'] )
            ? Yatoon_Manage_Booking::get_manage_url( $booking_data['manage_token'] )
            : '';
        $booking_data['calendar_url'] = YATOON_Calendar_Links::get_calendar_url( $booking_id, $booking_data['manage_token'] ?? '' );

        if (!empty($selected_options)) {
            foreach ($selected_options as $opt_id) {
                yatoon_db_insert($table_booking_options, array('booking_id'=>$booking_id,'option_id'=>$opt_id));
            }
        }

        if (get_option('yatoon_send_confirmation_email', '1') === '1') {
            $this->send_confirmation_email($booking_data, $service, $staff);
        }

        YATOON_Twilio::send_customer_confirmation($booking_data, $service, $staff);
        YATOON_Twilio::send_admin_alert($booking_data, $service, $staff);

        if (YATOON_Google_Calendar::is_enabled()) {
            $google_event_id = YATOON_Google_Calendar::add_event($booking_data, $service, $staff);
            if ($google_event_id) {
                yatoon_db_update($table_bookings, array('google_event_id'=>$google_event_id), array('id'=>$booking_id), array('%s'), array('%d'));
            }
        }

        // Schedule reminder + review request emails
        do_action( 'yatoon_booking_created', array_merge( $booking_data, array( 'booking_id' => $booking_id ) ) );

        wp_send_json_success(array(
            'message'      => __('Booking created successfully!', 'yatoon-booking-system'),
            'booking_id'   => $booking_id,
            'manage_url'   => $booking_data['manage_url'],
            'calendar_url' => $booking_data['calendar_url'],
            'service_name' => $service->name ?? '',
            'square_synced'=> true,
            'mode'         => 'square',
            'design_attachment_id' => $design_reference_fields['design_attachment_id'] ?? 0,
            'design_image_url' => $design_reference_fields['design_image_url'] ?? '',
            'design_upload_token' => $design_reference_fields['design_upload_token'] ?? '',
        ));
    }

    /**
     * Email the business owner about a new booking
     */
    private function notify_admin_new_booking( $booking, $service, $staff ) {
        $admin_email = get_option('yatoon_contact_email', '');
        if (empty($admin_email)) return;

        $business_name = get_option('yatoon_business_name', 'Your Business');
        $start         = new DateTime($booking['start_time']);
        $subject       = 'New Booking: ' . $service->name . ' - ' . $start->format('M j, Y g:i A');

        $body  = "New booking received!

";
        $body .= "Customer:  {$booking['customer_name']}
";
        $body .= "Email:     {$booking['customer_email']}
";
        $body .= "Phone:     {$booking['customer_phone']}
";
        $body .= "Service:   {$service->name}
";
        $body .= "Staff:     {$staff->name}
";
        $body .= "Date/Time: " . $start->format('l, F j, Y') . ' at ' . $start->format('g:i A') . "
";
        $body .= "Duration:  {$service->duration_minutes} min
";
        $body .= "Price:     \${$booking['total_price']}
";
        if (!empty($booking['notes'])) {
            $body .= "Notes:     {$booking['notes']}
";
        }
        if (!empty($booking['design_image_url'])) {
            $body .= "Design reference: {$booking['design_image_url']}
";
        }
        $body .= "
View in admin: " . admin_url('admin.php?page=sbs-bookings') . "
";
        $body .= "
  - {$business_name} Booking System";

        wp_mail($admin_email, $subject, $body);
    }

        /**
     * Send confirmation email
     */
    private function send_confirmation_email($booking, $service, $staff) {
        $to = $booking['customer_email'];
        
        // Get custom subject
        $subject = get_option('yatoon_email_subject', 'Booking Confirmation - {service_name}');
        
        $start_time = new DateTime($booking['start_time']);
        $end_time   = new DateTime($booking['end_time']);
        
        // Extract staff first name
        $staff_name       = $staff ? $staff->name : 'Your stylist';
        $staff_first_name = $this->get_first_name($staff_name);
        $business_name    = get_option('yatoon_business_name', get_bloginfo('name'));
        $business_phone   = get_option('yatoon_business_phone', get_option('yatoon_contact_phone', ''));
        $business_email   = get_option('yatoon_contact_email', '');
        $primary_color    = get_option('yatoon_primary_color', '#ec4899');

        // Choose email template style
        $template_style = get_option('yatoon_email_template_style', 'plain'); // plain | modern | minimal | branded
        
        // Get custom template
        $template = get_option('yatoon_email_template', 
'Dear {customer_name},

Your booking has been confirmed.

Service:    {service_name}
Option:     {option_names}
Staff:      {staff_name}
Date:       {date}
Time:       {time}
Duration:   {duration} minutes
Price:      ${total_price}

Payment: Please pay at the salon after your service. Thank you.

Notes: {notes}

Need to reschedule or cancel?
Use your self-service link (valid 7 days):
{manage_link}

Add this appointment to your calendar:
{calendar_link}

Thank you for choosing {business_name}.
We look forward to seeing you.

Best regards,
{business_name}');

        // Collect option names
        global $wpdb;
        $option_names = '';
        if (!empty($booking['selected_options'])) {
            $ids = array_map('intval', (array)$booking['selected_options']);
            if ($ids) {
                $placeholders = implode(',', array_fill(0, count($ids), '%d'));
                $opts = yatoon_db_get_col($wpdb->prepare(
                    "SELECT name FROM {$wpdb->prefix}yatoon_service_options WHERE id IN ($placeholders)",
                    ...$ids
                ));
                $option_names = implode(', ', $opts);
            }
        }
        if (empty($option_names)) $option_names = '-';

        // Replace all variables
        $variables = array(
            '{customer_name}',
            '{customer_first_name}',
            '{customer_email}',
            '{customer_phone}',
            '{service_name}',
            '{option_names}',
            '{staff_name}',
            '{staff_first_name}',
            '{date_time}',
            '{date}',
            '{time}',
            '{duration}',
            '{end_time}',
            '{total_price}',
            '{notes}',
            '{business_name}',
            '{business_phone}',
            '{business_email}',
            '{manage_link}',
            '{calendar_link}',
        );
        $name_parts = explode(' ', $booking['customer_name'], 2);

        // Build manage-booking link.
        // Local Only: include our self-service URL.
        // Square / Vagaro: those platforms send their own confirmation emails with
        // built-in cancel/reschedule links, so we show a note instead so customers
        // are not confused by two conflicting sets of links.
        // Generate manage link for all platforms (local, Square, Vagaro)
        $manage_link_text = '';
        if ( ! empty( $booking['manage_token'] ) && class_exists( 'Yatoon_Manage_Booking' ) ) {
            $manage_link_text = Yatoon_Manage_Booking::get_manage_url( $booking['manage_token'] );
        }
        $calendar_link_text = '';
        if ( ! empty( $booking['booking_id'] ) && ! empty( $booking['manage_token'] ) ) {
            $calendar_link_text = YATOON_Calendar_Links::get_calendar_url( $booking['booking_id'], $booking['manage_token'] );
        }

        $replacements = array(
            $booking['customer_name'],
            $name_parts[0],
            $booking['customer_email'],
            $booking['customer_phone'],
            $service->name,
            $option_names,
            $staff_name,
            $staff_first_name,
            $start_time->format('F j, Y g:i A'),
            $start_time->format('F j, Y'),
            $start_time->format('g:i A'),
            $service->duration_minutes,
            $end_time->format('g:i A'),
            number_format($booking['total_price'], 2),
            $booking['notes'] ?: '-',
            $business_name,
            $business_phone ?: '-',
            $business_email,
            $manage_link_text,
            $calendar_link_text ?: '-',
        );

        // Subject replacement
        $subject = str_replace($variables, $replacements, $subject);
        $message_body = str_replace($variables, $replacements, $template);

        // Keep self-service links visible even when an older custom email template is still saved.
        $action_lines = array();
        if ($manage_link_text) {
            $action_lines[] = 'Manage, reschedule, or cancel: ' . $manage_link_text;
        }
        if ($calendar_link_text) {
            $action_lines[] = 'Add to calendar: ' . $calendar_link_text;
        }
        // WhatsApp link
        if (get_option('yatoon_whatsapp_enabled', '0') === '1') {
            $wa_number = preg_replace('/[^0-9]/', '', get_option('yatoon_whatsapp_number', get_option('yatoon_business_phone', '')));
            $wa_prefill = get_option('yatoon_whatsapp_prefill', 'Hi! I just booked an appointment.');
            if ($wa_number) {
                $action_lines[] = 'Message us on WhatsApp: https://wa.me/' . $wa_number . '?text=' . rawurlencode($wa_prefill);
            }
        }
        if (!empty($action_lines) && strpos($template, '{manage_link}') === false && strpos($template, '{calendar_link}') === false) {
            $message_body .= "\n\n" . implode("\n", $action_lines);
        }

        // Wrap in HTML template if style is not plain
        if ($template_style !== 'plain') {
            $message_body = $this->wrap_email_html($message_body, $template_style, $primary_color, $business_name);
            $headers = array('Content-Type: text/html; charset=UTF-8');
        } else {
            $headers = array();
        }
        
        wp_mail($to, $subject, $message_body, $headers);
        
        // Also notify admin if enabled
        if (get_option('yatoon_email_notify_admin', '0') === '1') {
            $admin_subject = '[New Booking] ' . $booking['customer_name'] . ' - ' . $service->name . ' on ' . $start_time->format('M j, Y g:i A');
            $admin_body = "New booking received!\n\n"
                . "Customer: " . $booking['customer_name'] . "\n"
                . "Email: "    . $booking['customer_email'] . "\n"
                . "Phone: "    . $booking['customer_phone'] . "\n"
                . "Service: "  . $service->name . "\n"
                . "Option: "   . $option_names . "\n"
                . "Staff: "    . $staff_name . "\n"
                . "Date/Time: ". $start_time->format('F j, Y g:i A') . "\n"
                . "Price: $"   . number_format($booking['total_price'], 2) . "\n"
                . "Notes: "    . ($booking['notes'] ?: '-') . "\n";
            wp_mail($business_email, $admin_subject, $admin_body);
        }
    }

    /**
     * Wrap plain-text email body in an HTML email template
     */
    private function wrap_email_html($body_text, $style, $primary_color, $business_name) {
        // Convert plain text to simple HTML paragraphs
        $html_body = nl2br(esc_html($body_text));

        if ($style === 'modern') {
            return '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body style="margin:0;padding:0;background:#f4f4f5;font-family:\'Helvetica Neue\',Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 16px;">
  <tr><td align="center">
    <table width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.08);">
      <tr><td style="background:' . esc_attr($primary_color) . ';padding:32px 40px;text-align:center;">
        ' . ( get_option('yatoon_business_logo') ? '<img src="' . esc_url(get_option('yatoon_business_logo')) . '" alt="' . esc_attr($business_name) . '" style="height:60px;max-width:200px;object-fit:contain;display:block;margin:0 auto 16px;" />' : '' ) . '
        <h1 style="color:#fff;margin:0;font-size:28px;font-weight:700;">' . esc_html($business_name) . '</h1>
          <p style="color:rgba(255,255,255,.85);margin:8px 0 0;font-size:16px;">Booking Confirmed</p>
      </td></tr>
      <tr><td style="padding:40px;color:#374151;font-size:15px;line-height:1.7;">' . $html_body . '</td></tr>
      <tr><td style="background:#f9fafb;padding:24px 40px;text-align:center;border-top:1px solid #e5e7eb;">
        ' . ( YATOON_License::is_pro() ? '' : '<p style="margin:0;color:#9ca3af;font-size:13px;">Powered by Yatoon Booking System</p>' ) . '
      </td></tr>
    </table>
  </td></tr>
</table></body></html>';
        } elseif ($style === 'minimal') {
            return '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="margin:0;padding:40px 16px;background:#fff;font-family:Georgia,serif;color:#1f2937;max-width:560px;margin:auto;">
' . ( get_option('yatoon_business_logo') ? '<img src="' . esc_url(get_option('yatoon_business_logo')) . '" alt="' . esc_attr($business_name) . '" style="height:50px;max-width:180px;object-fit:contain;display:block;margin:0 auto 16px;" />' : '<h2 style="color:' . esc_attr($primary_color) . ';border-bottom:2px solid ' . esc_attr($primary_color) . ';padding-bottom:12px;">' . esc_html($business_name) . '</h2>' ) . '
<div style="line-height:1.8;font-size:15px;">' . $html_body . '</div>
<p style="margin-top:40px;font-size:12px;color:#9ca3af;border-top:1px solid #e5e7eb;padding-top:16px;">Yatoon Booking System</p>
</body></html>';
        } elseif ($style === 'branded') {
            return '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body style="margin:0;padding:0;background:#1f2937;font-family:\'Helvetica Neue\',Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#1f2937;padding:40px 16px;">
  <tr><td align="center">
    <table width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#111827;border-radius:16px;overflow:hidden;">
      <tr><td style="padding:40px;text-align:center;border-bottom:1px solid #374151;">
        ' . ( get_option('yatoon_business_logo') ? '<img src="' . esc_url(get_option('yatoon_business_logo')) . '" alt="' . esc_attr($business_name) . '" style="height:56px;max-width:180px;object-fit:contain;display:block;margin:0 auto 12px;filter:brightness(0) invert(1);" />' : '' ) . '
        <div style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' . esc_attr($primary_color) . ';margin-right:8px;"></div>
        <span style="color:#fff;font-size:22px;font-weight:700;">' . esc_html($business_name) . '</span>
      </td></tr>
      <tr><td style="padding:40px;color:#d1d5db;font-size:15px;line-height:1.8;">' . $html_body . '</td></tr>
      <tr><td style="padding:24px 40px;text-align:center;border-top:1px solid #374151;">
        <p style="margin:0;color:#4b5563;font-size:12px;">Yatoon Booking System</p>
      </td></tr>
    </table>
  </td></tr>
</table></body></html>';
        }

        // Fallback - plain text
        return $body_text;
    }
    
    /**
     * Extract first name
     */
    private function get_first_name($full_name) {
        if (empty($full_name)) {
            return '';
        }
        // Extract first word
        $parts = preg_split('/\s+/', trim($full_name));
        return $parts[0];
    }

    private function get_client_ip() {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '';
    }

    /**
     * Verify a Cloudflare Turnstile token server-side.
     * Invisible to real visitors - only blocks requests that never solved
     * the challenge a real browser runs automatically in the background.
     */
    private function verify_turnstile($token) {
        $secret = get_option('yatoon_turnstile_secret_key');
        if (empty($secret) || empty($token)) {
            return false;
        }
        $response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', array(
            'body'    => array(
                'secret'   => $secret,
                'response' => $token,
                'remoteip' => $this->get_client_ip(),
            ),
            'timeout' => 10,
        ));
        if (is_wp_error($response)) {
            // Fail open on network errors - don't block real customers because
            // Cloudflare's endpoint had a hiccup.
            return get_option('yatoon_turnstile_fail_closed', '0') !== '1';
        }
        $body = json_decode(wp_remote_retrieve_body($response), true);
        return !empty($body['success']);
    }

    private function normalize_phone_digits($phone) {
        $digits = preg_replace('/\D/', '', (string) $phone);
        if (strlen($digits) === 11 && substr($digits, 0, 1) === '1') {
            $digits = substr($digits, 1);
        }
        return $digits;
    }

    private function enforce_public_rate_limit($scope, $limit = 5, $minutes = 10) {
        $client_ip = $this->get_client_ip();
        if ($client_ip === '') {
            return;
        }

        $key = 'yatoon_public_' . sanitize_key($scope) . '_' . md5($client_ip);
        $count = (int) get_transient($key);
        if ($count >= $limit) {
            wp_send_json_error(array(
                'message' => sprintf(
                    __('Too many requests. Please wait %d minutes and try again.', 'yatoon-booking-system'),
                    $minutes
                )
            ));
        }

        set_transient($key, $count + 1, $minutes * MINUTE_IN_SECONDS);
    }
    
    /**
     * Join waitlist
     */
    public function join_waitlist() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');

        $this->enforce_public_rate_limit('waitlist', 5, 10);
        
        $name           = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
        $email          = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
        $phone          = isset($_POST['phone']) ? sanitize_text_field(wp_unslash($_POST['phone'])) : '';
        $preferred_date = isset($_POST['preferred_date']) ? sanitize_text_field(wp_unslash($_POST['preferred_date'])) : '';
        $preferred_time = isset($_POST['preferred_time']) ? sanitize_text_field(wp_unslash($_POST['preferred_time'])) : 'any';
        $flexible_dates = isset($_POST['flexible_dates']) ? 1 : 0;
        $flexible_times = isset($_POST['flexible_times']) ? 1 : 0;
        $notes          = isset($_POST['notes']) ? sanitize_textarea_field(wp_unslash($_POST['notes'])) : '';
        
        // Multi-select services - sanitize array
        $service_ids = array();
        if (!empty($_POST['service_ids']) && is_array($_POST['service_ids'])) {
            $service_ids = array_map('intval', $_POST['service_ids']);
            $service_ids = array_filter($service_ids); // remove zeros
        }
        // Fallback: legacy single service_id field
        if (empty($service_ids) && !empty($_POST['service_id'])) {
            $service_ids = array(intval($_POST['service_id']));
        }
        
        // Validation
        $errors = array();
        if (empty($name))           $errors[] = 'name is required';
        if (empty($email))          $errors[] = 'email is required';
        if (empty($phone))          $errors[] = 'phone is required';
        if (empty($service_ids))    $errors[] = 'please select at least one service';
        if (empty($preferred_date)) $errors[] = 'preferred date is required';
        
        if (!empty($errors)) {
            wp_send_json_error(array('message' => ucfirst(implode(', ', $errors)) . '.'));
        }
        
        global $wpdb;
        $table = $wpdb->prefix . 'yatoon_waitlist';

        $phone_digits = $this->normalize_phone_digits($phone);
        $duplicate = yatoon_db_get_var($wpdb->prepare(
            "SELECT id FROM $table
             WHERE service_id = %d
               AND preferred_date = %s
               AND status = 'active'
               AND (LOWER(customer_email) = LOWER(%s)
                    OR RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(customer_phone,'-',''),' ',''),'(',''),')',''),'+',''),'.',''),'\t',''), 10) = %s)
             LIMIT 1",
            $service_ids[0],
            $preferred_date,
            $email,
            $phone_digits
        ));
        if ($duplicate) {
            wp_send_json_error(array('message' => __('You are already on the waitlist for this date and service.', 'yatoon-booking-system')));
        }
        
        // Look up service names for email/notes
        $placeholders  = implode(',', array_fill(0, count($service_ids), '%d'));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $service_rows  = yatoon_db_get_results(
            $wpdb->prepare("SELECT id, name FROM {$wpdb->prefix}yatoon_services WHERE id IN ($placeholders)", ...$service_ids)
        );
        $service_names = wp_list_pluck($service_rows, 'name');
        
        // Store as one row per service (keeps schema clean) or one combined row
        // We'll store one row with service_ids as comma-separated in notes, and first service_id as primary
        $primary_service_id = $service_ids[0];
        $services_note = 'Services: ' . implode(', ', $service_names);
        if (count($service_ids) > 1) {
            $notes = $services_note . ($notes ? "\n\n" . $notes : '');
        }
        
        $result = yatoon_db_insert($table, array(
            'customer_name'  => $name,
            'customer_email' => $email,
            'customer_phone' => $phone,
            'service_id'     => $primary_service_id,
            'staff_id'       => null,
            'preferred_date' => $preferred_date,
            'preferred_time' => $preferred_time,
            'flexible_dates' => $flexible_dates,
            'flexible_times' => $flexible_times,
            'notes'          => $notes,
            'status'         => 'active',
        ));
        
        if ($result) {
            // Customer confirmation email
            $this->send_waitlist_confirmation_v2($name, $email, $service_names, $preferred_date, $preferred_time);
            // Admin notification
            $this->notify_admin_waitlist_v2($name, $email, $phone, $service_names, $preferred_date, $preferred_time, $notes);
            
            wp_send_json_success(array('message' => 'Successfully joined waitlist'));
        } else {
            wp_send_json_error(array('message' => 'Database error. Please try again.'));
        }
    }
    
    /**
     * Enhanced waitlist customer confirmation (multi-service)
     */
    private function send_waitlist_confirmation_v2($name, $email, $service_names, $date, $time_pref) {
        $business_name  = get_option('yatoon_business_name', 'Your Business');
        $business_email = get_option('yatoon_contact_email', '');
        $subject        = "You're on our waitlist - $business_name";
        
        $time_labels = array(
            'morning'       => 'Morning (10am - 12pm)',
            'afternoon'     => 'Afternoon (12pm - 4pm)',
            'late_afternoon'=> 'Late Afternoon (4pm - 7pm)',
            'any'           => 'Any time',
        );
        $time_label = $time_labels[$time_pref] ?? 'Any time';
        $services_str = implode(', ', $service_names);
        $date_fmt = date('F j, Y', strtotime($date));
        
        $message  = "Hi $name,\n\n";
        $message .= "Thank you for joining our waitlist!\n\n";
        $message .= "---------------------\n";
        $message .= "Service(s):     $services_str\n";
        $message .= "Preferred Date: $date_fmt\n";
        $message .= "Preferred Time: $time_label\n";
        $message .= "---------------------\n\n";
        $message .= "We'll contact you as soon as a spot opens up.\n\n";
        $message .= "Best regards,\n$business_name";
        
        $headers = array();
        if ($business_email) {
            $headers[] = 'From: ' . $business_name . ' <' . $business_email . '>';
        }
        wp_mail($email, $subject, $message, $headers);
    }
    
    /**
     * Enhanced waitlist admin notification (multi-service)
     */
    private function notify_admin_waitlist_v2($name, $email, $phone, $service_names, $date, $time_pref, $notes) {
        $business_name  = get_option('yatoon_business_name', 'Your Business');
        $admin_email    = get_option('yatoon_contact_email', '');
        if (empty($admin_email)) return;
        
        $time_labels = array(
            'morning'       => 'Morning (10am - 12pm)',
            'afternoon'     => 'Afternoon (12pm - 4pm)',
            'late_afternoon'=> 'Late Afternoon (4pm - 7pm)',
            'any'           => 'Any time',
        );
        $time_label   = $time_labels[$time_pref] ?? 'Any time';
        $services_str = implode(', ', $service_names);
        $date_fmt     = date('F j, Y', strtotime($date));
        
        $subject  = "New Waitlist Request - $name ($business_name)";
        $message  = "New waitlist request received:\n\n";
        $message .= "Customer:      $name\n";
        $message .= "Email:         $email\n";
        $message .= "Phone:         $phone\n";
        $message .= "Service(s):    $services_str\n";
        $message .= "Preferred Date: $date_fmt\n";
        $message .= "Preferred Time: $time_label\n";
        if ($notes) {
            $message .= "\nNotes:\n$notes\n";
        }
        $message .= "\nCheck your admin panel for details.";
        
        wp_mail($admin_email, $subject, $message);
    }
    
    /**
     * Send waitlist confirmation email to customer
     */
    private function send_waitlist_confirmation($name, $email, $service_id, $date) {
        global $wpdb;
        $service = yatoon_db_get_row($wpdb->prepare(
            "SELECT name FROM {$wpdb->prefix}yatoon_services WHERE id = %d",
            $service_id
        ));
        
        $to = $email;
        $business_name = get_option('yatoon_business_name', 'Your Business');
        $subject = 'You\'re on our waitlist - ' . $business_name;
        $message = sprintf(
            "Hi %s,\n\nThank you for joining our waitlist!\n\nService: %s\nPreferred Date: %s\n\nWe'll contact you as soon as a spot becomes available.\n\nBest regards,\n%s Team",
            $name,
            $service->name,
            date('F j, Y', strtotime($date)),
            $business_name
        );
        
        wp_mail($to, $subject, $message);
    }
    
    /**
     * Notify admin of a new waitlist request
     */
    private function notify_admin_waitlist($name, $email, $phone, $service_id, $date, $notes = '') {
        global $wpdb;
        $service = yatoon_db_get_row($wpdb->prepare(
            "SELECT name FROM {$wpdb->prefix}yatoon_services WHERE id = %d",
            $service_id
        ));
        
        $admin_email = get_option('yatoon_contact_email', '');
        $business_name = get_option('yatoon_business_name', 'Your Business');
        $subject = 'New Waitlist Request - ' . $business_name;
        
        // Build email content with full notes
        $message = "New waitlist request:\n\n";
        $message .= "Customer: " . $name . "\n";
        $message .= "Email: " . $email . "\n";
        $message .= "Phone: " . $phone . "\n";
        $message .= "Service: " . $service->name . "\n";
        $message .= "Preferred Date: " . date('F j, Y', strtotime($date)) . "\n\n";
        
        if (!empty($notes)) {
            $message .= "Customer Notes:\n";
            $message .= "-------------------\n";
            $message .= $notes . "\n";
            $message .= "-------------------\n\n";
        }
        
        $message .= "Check your admin panel for details.";
        
        wp_mail($admin_email, $subject, $message);
    }


    /**
     * Admin diagnostic: show raw yatoon_bookings rows for a given date.
     * Helps identify bad end_time / timezone records causing false conflicts.
     * Usage: POST to admin-ajax.php with action=yatoon_debug_date_bookings&date=2026-02-24
     */

    /**
     * Admin diagnostic: directly query Square availability API for a specific date + service variation.
     * Shows raw slots returned by Square, staff schedules, and why certain times are unavailable.
     */

    /**
     * Admin: cancel a booking - updates local DB status to cancelled and cancels in Square.
     */

    /**
     * Admin: permanently delete a single booking row from local yatoon_bookings.
     * Does NOT call Square - use cancel first if booking is active in Square.
     */

    /**
     * Admin: Pull latest booking statuses from Square and update local yatoon_bookings.
     *
     * What it does:
     * 1. Fetch all Square bookings for the past 30 days + next 60 days
     * 2. For each local booking with a square_booking_id, update status to match Square
     * 3. For Square bookings not in local DB, optionally insert them
     * 4. Mark local bookings NOT found in Square (e.g. deleted) as cancelled
     *
     * This fixes the "No available times" false positive caused by cancelled Square
     * bookings still showing as "confirmed" in the local DB.
     */
    public function sync_bookings_from_square() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => 'Permission denied'));
        }
        check_ajax_referer('yatoon_admin_nonce', 'nonce');

        global $wpdb;
        $table          = $wpdb->prefix . 'yatoon_bookings';
        $table_staff    = $wpdb->prefix . 'yatoon_staff';
        $table_services = $wpdb->prefix . 'yatoon_services';
        $table_opts     = $wpdb->prefix . 'yatoon_service_options';
        $timezone       = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz_obj         = new DateTimeZone($timezone);

        // Date range: past 30 days to future 60 days
        $start_at = (new DateTime('-30 days', $tz_obj))->format('Y-m-d\TH:i:s\Z');
        $end_at   = (new DateTime('+60 days', $tz_obj))->format('Y-m-d\TH:i:s\Z');

        // Pull all bookings from Square
        $square_bookings = $this->square_api->list_all_bookings($start_at, $end_at);

        if (empty($square_bookings)) {
            wp_send_json_success(array(
                'message'  => 'Square returned 0 bookings in the date range. Local DB unchanged.',
                'updated'  => 0,
                'inserted' => 0,
                'cancelled'=> 0,
            ));
        }

        // Build a map: square_booking_id => Square booking object
        $sq_map = array();
        foreach ($square_bookings as $sb) {
            if (!empty($sb['id'])) $sq_map[$sb['id']] = $sb;
        }

        // Map Square status -> local status
        $status_map = array(
            'ACCEPTED'   => 'confirmed',
            'PENDING'    => 'pending',
            'CANCELLED_BY_CUSTOMER'  => 'cancelled',
            'CANCELLED_BY_SELLER'    => 'cancelled',
            'DECLINED'   => 'cancelled',
            'NO_SHOW'    => 'no_show',
        );

        $updated   = 0;
        $inserted  = 0;
        $cancelled = 0;

        // Get all local bookings that have a Square ID in our date window
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $local_rows = yatoon_db_get_results(
            "SELECT id, square_booking_id, status, start_time, end_time,
                    customer_name, customer_email, customer_phone FROM $table
             WHERE square_booking_id IS NOT NULL AND square_booking_id != ''
               AND start_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
               AND start_time <= DATE_ADD(NOW(), INTERVAL 60 DAY)"
        );

        $local_sq_ids = array();
        $customer_cache = array();
        $reconciled_square_ids = array();
        foreach ($local_rows as $reconcile_local) {
            $reconcile_id = (string) $reconcile_local->square_booking_id;
            if (isset($reconciled_square_ids[$reconcile_id]) || !isset($sq_map[$reconcile_id])) continue;
            $reconciled_square_ids[$reconcile_id] = true;
            $updated += yatoon_reconcile_square_booking_segments($sq_map[$reconcile_id]);
        }
        foreach ($local_rows as $local) {
            if (empty($local->square_booking_id)) continue;
            $local_sq_ids[] = $local->square_booking_id;

            if (!isset($sq_map[$local->square_booking_id])) {
                // Not found in Square at all - mark as cancelled if still active
                if (!in_array($local->status, array('cancelled','no_show'))) {
                    yatoon_db_update($table, array('status' => 'cancelled'), array('id' => $local->id));
                    $cancelled++;
                }
                continue;
            }

            $sq_booking  = $sq_map[$local->square_booking_id];
            $sq_status   = isset($sq_booking['status']) ? $sq_booking['status'] : '';
            $local_new   = isset($status_map[$sq_status]) ? $status_map[$sq_status] : null;

            // Recompute end_time from Square segment data (fixes stale/zero end_time in DB)
            $update_fields = array();
            $sq_customer_id = $sq_booking['customer_id'] ?? '';
            $needs_customer = !$local->customer_name
                || strcasecmp(trim($local->customer_name), 'Square Customer') === 0
                || (!$local->customer_email && !$local->customer_phone);
            if ($sq_customer_id && $needs_customer) {
                if (!array_key_exists($sq_customer_id, $customer_cache)) {
                    $customer_cache[$sq_customer_id] = $this->square_api->get_customer($sq_customer_id);
                }
                $sq_customer = $customer_cache[$sq_customer_id];
                if ($sq_customer) {
                    $real_name = trim(($sq_customer['given_name'] ?? '') . ' ' . ($sq_customer['family_name'] ?? ''));
                    if ($real_name) $update_fields['customer_name'] = $real_name;
                    if (!empty($sq_customer['email_address'])) $update_fields['customer_email'] = $sq_customer['email_address'];
                    if (!empty($sq_customer['phone_number'])) $update_fields['customer_phone'] = $sq_customer['phone_number'];
                }
            }
            if ($local_new && $local_new !== $local->status) {
                $update_fields['status'] = $local_new;
            }
            if (!empty($update_fields)) {
                yatoon_db_update($table, $update_fields, array('id' => $local->id));
                $updated++;
            }
        }

        // Also check for Square bookings NOT in local DB at all -> insert them
        foreach ($square_bookings as $sb) {
            if (empty($sb['id'])) continue;
            if (in_array($sb['id'], $local_sq_ids)) continue; // already handled above

            // Skip cancelled bookings - no point inserting them
            $sq_status  = $sb['status'] ?? '';
            $local_status = isset($status_map[$sq_status]) ? $status_map[$sq_status] : 'confirmed';
            if ($local_status === 'cancelled') continue;

            // Try to map Square booking fields to local DB
            if (empty($sb['appointment_segments'])) continue;
            $seg       = $sb['appointment_segments'][0];
            $var_id    = $seg['service_variation_id'] ?? '';
            $sq_staff  = $seg['team_member_id'] ?? '';
            $start_at_sq = $sb['start_at'] ?? '';
            if (!$var_id || !$start_at_sq) continue;

            // Look up local IDs
            $opt = yatoon_db_get_row($wpdb->prepare("SELECT id, service_id, duration_minutes FROM $table_opts WHERE square_id=%s LIMIT 1", $var_id));
            if (!$opt) continue;
            $staff = yatoon_db_get_row($wpdb->prepare("SELECT id FROM $table_staff WHERE square_id=%s LIMIT 1", $sq_staff));
            if (!$staff) continue;

            $start_dt = new DateTime($start_at_sq);
            $start_dt->setTimezone($tz_obj);
            $end_dt   = clone $start_dt;

            // Duration: prefer Square segment value, fall back to local option/service duration.
            // Sum ALL segments so multi-service bookings get correct total end_time.
            $total_dur_min = 0;
            if (!empty($sb['appointment_segments'])) {
                foreach ($sb['appointment_segments'] as $seg_item) {
                    $seg_dur = intval($seg_item['duration_minutes'] ?? 0);
                    if ($seg_dur > 0) {
                        $total_dur_min += $seg_dur;
                    } else {
                        // Fall back to local option duration for this segment
                        $seg_var = $seg_item['service_variation_id'] ?? '';
                        if ($seg_var) {
                            $seg_opt = yatoon_db_get_var($wpdb->prepare(
                                "SELECT duration_minutes FROM $table_opts WHERE square_id=%s LIMIT 1", $seg_var
                            ));
                            if ($seg_opt) $total_dur_min += intval($seg_opt);
                        }
                    }
                }
            }
            // Final fallback: use the first segment's local option duration
            if ($total_dur_min <= 0) $total_dur_min = max(intval($opt->duration_minutes), 30);
            $end_dt->modify("+{$total_dur_min} minutes");

            $customer = $sb['customer_note'] ?? '';
            $sq_customer = null;
            $sq_customer_id = $sb['customer_id'] ?? '';
            if ($sq_customer_id) {
                if (!array_key_exists($sq_customer_id, $customer_cache)) {
                    $customer_cache[$sq_customer_id] = $this->square_api->get_customer($sq_customer_id);
                }
                $sq_customer = $customer_cache[$sq_customer_id];
                if ($sq_customer) {
                    $customer = trim(($sq_customer['given_name'] ?? '') . ' ' . ($sq_customer['family_name'] ?? '')) ?: $customer;
                }
            }

            yatoon_db_insert($table, array(
                'square_booking_id' => $sb['id'],
                'customer_name'     => $customer ?: 'Square Customer',
                'customer_email'    => $sq_customer['email_address'] ?? '',
                'customer_phone'    => $sq_customer['phone_number'] ?? '',
                'service_id'        => $opt->service_id,
                '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'            => $local_status,
                'square_synced'     => 1,
                'total_price'       => 0,
            ));
            $new_bk_id = $wpdb->insert_id;
            // Insert booking_options for each segment so service names show correctly
            if ( $new_bk_id && !empty($sb['appointment_segments']) ) {
                foreach ($sb['appointment_segments'] as $seg_item2) {
                    $sv2 = $seg_item2['service_variation_id'] ?? '';
                    if (!$sv2) continue;
                    $so2 = yatoon_db_get_row($wpdb->prepare("SELECT id FROM $table_opts WHERE square_id=%s LIMIT 1", $sv2)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                    if ($so2) yatoon_db_insert($table_booking_options, array('booking_id'=>$new_bk_id,'option_id'=>$so2->id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                }
            }
            $inserted++;
        }

        wp_send_json_success(array(
            'message'   => sprintf(
                'Sync complete: %d status(es) updated, %d new booking(s) inserted, %d removed-from-Square marked cancelled.',
                $updated, $inserted, $cancelled
            ),
            'updated'   => $updated,
            'inserted'  => $inserted,
            'cancelled' => $cancelled,
        ));
    }

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

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

        $table = $wpdb->prefix . 'yatoon_bookings';
        $table_opts = $wpdb->prefix . 'yatoon_booking_options';

        // Delete associated booking options first
        yatoon_db_delete($table_opts, array('booking_id' => $id));
        // Delete the booking row
        $deleted = yatoon_db_delete($table, array('id' => $id));

        if ($deleted === false) {
            wp_send_json_error(array('message' => 'Database delete failed'));
        }
        wp_send_json_success(array('message' => 'Booking #' . $id . ' deleted.'));
    }

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

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

        $table = $wpdb->prefix . 'yatoon_bookings';

        // Get the booking (and all siblings in same group)
        $booking = yatoon_db_get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
        if (!$booking) {
            wp_send_json_error(array('message' => 'Booking not found'));
        }

        // Collect all IDs to cancel (solo or entire group)
        $ids_to_cancel = array();
        if (!empty($booking->group_booking_id)) {
            $group_rows = yatoon_db_get_results($wpdb->prepare(
                "SELECT id, square_booking_id FROM $table WHERE group_booking_id = %s",
                $booking->group_booking_id
            ));
            foreach ($group_rows as $g) $ids_to_cancel[] = $g;
        } else {
            $ids_to_cancel[] = $booking;
        }

        $sq_errors = array();
        foreach ($ids_to_cancel as $bk) {
            // Cancel in Square if synced
            if (!empty($bk->square_booking_id)) {
                // Get current version from Square
                $sq_resp = $this->square_api->get_booking($bk->square_booking_id);
                $version = null;
                if (!is_wp_error($sq_resp) && isset($sq_resp['booking']['version'])) {
                    $version = intval($sq_resp['booking']['version']);
                }
                $cancel_resp = $this->square_api->cancel_booking($bk->square_booking_id, $version);
                if (is_wp_error($cancel_resp)) {
                    $sq_errors[] = $cancel_resp->get_error_message();
                }
            }
            // Update local DB status to cancelled
            yatoon_db_update($table, array('status' => 'cancelled'), array('id' => intval($bk->id)));
        }

        if (!empty($sq_errors)) {
            wp_send_json_success(array(
                'message'   => 'Booking cancelled locally. Square warning: ' . implode('; ', $sq_errors),
                'cancelled' => count($ids_to_cancel),
            ));
        } else {
            // notify_method: 'none' | 'email' | 'sms' | 'both'. Falls back to the legacy
            // boolean notify_customer (checkbox) so any cached/old JS keeps working.
            $notify_method = isset($_POST['notify_method']) ? sanitize_key(wp_unslash($_POST['notify_method'])) : '';
            if (!in_array($notify_method, array('none', 'email', 'sms', 'both'), true)) {
                $notify_method = (!empty($_POST['notify_customer']) && intval($_POST['notify_customer']) === 1) ? 'email' : 'none';
            }

            if ($notify_method !== 'none') {
                // Re-fetch the primary booking with service/staff names joined in - the
                // group-booking query above only selects id/square_booking_id, and the
                // raw bookings row never has service_name/staff_name columns.
                $primary_id = intval($ids_to_cancel[0]->id);
                $primary    = yatoon_db_get_row($wpdb->prepare(
                    "SELECT b.*, s.name AS service_name, st.name AS staff_name
                     FROM $table 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",
                    $primary_id
                ));

                if ($primary) {
                    $notify_data = array(
                        'customer_name'  => $primary->customer_name  ?? '',
                        'customer_email' => $primary->customer_email ?? '',
                        'customer_phone' => $primary->customer_phone ?? '',
                        'service_name'   => $primary->service_name   ?? '',
                        'staff_name'     => $primary->staff_name     ?? '',
                        'start_time'     => $primary->start_time     ?? '',
                        'end_time'       => $primary->end_time       ?? '',
                        'total_price'    => $primary->total_price    ?? 0,
                        'notes'          => $primary->notes          ?? '',
                    );

                    if (in_array($notify_method, array('email', 'both'), true)) {
                        $notif = new YATOON_Notifications();
                        $notif->send_cancel_email($notify_data);
                    }
                    if (in_array($notify_method, array('sms', 'both'), true) && class_exists('YATOON_Twilio')) {
                        YATOON_Twilio::send_cancel_sms($notify_data);
                    }
                }
            }
            wp_send_json_success(array(
                'message'   => 'Booking cancelled successfully.',
                'cancelled' => count($ids_to_cancel),
            ));
        }
    }

    public function debug_square_availability() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        global $wpdb;
        $date       = isset($_POST['date'])       ? sanitize_text_field( wp_unslash( $_POST['date'] ) )  : date('Y-m-d');
        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id'])         : 0;

        $timezone = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz_obj   = new DateTimeZone($timezone);
        $date_start = (new DateTime($date . ' 00:00:00', $tz_obj))->format('c');
        $date_end   = (new DateTime($date . ' 23:59:59', $tz_obj))->format('c');

        // Get all eligible staff for this service
        $staff_list = yatoon_db_get_results($wpdb->prepare(
            "SELECT st.id, st.name, st.square_id
             FROM {$wpdb->prefix}yatoon_staff st
             INNER JOIN {$wpdb->prefix}yatoon_staff_services ss ON st.id = ss.staff_id
             WHERE st.is_active=1 AND st.is_bookable=1 AND ss.service_id=%d
             ORDER BY st.sort_order ASC, st.name ASC",
            $service_id
        ));
        if (empty($staff_list)) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $staff_list = yatoon_db_get_results(
                "SELECT id, name, square_id FROM {$wpdb->prefix}yatoon_staff WHERE is_active=1 AND is_bookable=1 ORDER BY sort_order ASC, name ASC"
            );
        }

        // Get all variations for this service
        $variations = yatoon_db_get_results($wpdb->prepare(
            "SELECT id, name, square_id, duration_minutes, group_name FROM {$wpdb->prefix}yatoon_service_options WHERE service_id=%d AND square_id!='' ORDER BY sort_order ASC, id ASC",
            $service_id
        ));

        $service = yatoon_db_get_row($wpdb->prepare("SELECT name FROM {$wpdb->prefix}yatoon_services WHERE id=%d", $service_id));

        $results_per_staff = array();
        foreach ($staff_list as $staff) {
            if (empty($staff->square_id)) continue;
            $staff_result = array(
                'staff_id'    => $staff->id,
                'staff_name'  => $staff->name,
                'staff_sq_id' => $staff->square_id,
                'variations'  => array(),
            );
            foreach ($variations as $var) {
                $resp = $this->square_api->search_availability($var->square_id, $date_start, $date_end, $staff->square_id);
                $slots = array();
                $sq_error = null;
                if (is_wp_error($resp)) {
                    $sq_error = $resp->get_error_message();
                } elseif (isset($resp['availabilities'])) {
                    foreach ($resp['availabilities'] as $av) {
                        if (empty($av['start_at'])) continue;
                        $t = new DateTime($av['start_at']);
                        $t->setTimezone($tz_obj);
                        $slots[] = $t->format('g:i A');
                    }
                } elseif (isset($resp['errors'])) {
                    $sq_error = json_encode($resp['errors']);
                }
                $staff_result['variations'][] = array(
                    'var_name'       => $var->name,
                    'var_sq_id'      => $var->square_id,
                    'duration_min'   => $var->duration_minutes,
                    'group_name'     => $var->group_name,
                    'slots_count'    => count($slots),
                    'slots'          => $slots,
                    'error'          => $sq_error,
                );
            }
            $results_per_staff[] = $staff_result;
        }

        wp_send_json_success(array(
            'date'             => $date,
            'service_id'       => $service_id,
            'service_name'     => $service ? $service->name : "id=$service_id",
            'timezone'         => $timezone,
            'staff_count'      => count($results_per_staff),
            'variation_count'  => count($variations),
            'results_per_staff'=> $results_per_staff,
        ));
    }

    public function debug_date_bookings() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        check_ajax_referer('yatoon_admin_nonce', 'nonce');
        global $wpdb;
        $date = isset($_POST['date']) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : date('Y-m-d');
        $timezone  = ( wp_timezone_string() ?: 'America/Chicago' );
        $tz_obj    = new DateTimeZone($timezone);
        $tz_utc    = new DateTimeZone('UTC');

        // All bookings on this date (not cancelled/no_show)
        // Show ALL records regardless of status - this reveals "confirmed" records
        // that should have been cancelled but weren't updated in the local DB.
        $rows = yatoon_db_get_results($wpdb->prepare(
            "SELECT b.id, b.square_booking_id, b.customer_name, b.service_id, b.staff_id,
                    b.start_time, b.end_time, b.status, b.square_synced,
                    s.name AS service_name, st.name AS staff_name
             FROM {$wpdb->prefix}yatoon_bookings b
             LEFT JOIN {$wpdb->prefix}yatoon_services s  ON b.service_id = s.id
             LEFT JOIN {$wpdb->prefix}yatoon_staff   st ON b.staff_id   = st.id
             WHERE DATE(b.start_time) = %s
             ORDER BY b.status ASC, b.start_time ASC",
            $date
        ));

        $result = array();
        foreach ($rows as $r) {
            // Parse stored time (assumed local tz) and compute UTC
            try {
                $start_local = new DateTime($r->start_time, $tz_obj);
                $end_local   = new DateTime($r->end_time,   $tz_obj);
                $start_utc   = clone $start_local; $start_utc->setTimezone($tz_utc);
                $end_utc     = clone $end_local;   $end_utc->setTimezone($tz_utc);
                $duration_min = round(($end_local->getTimestamp() - $start_local->getTimestamp()) / 60);
            } catch (Exception $e) {
                $start_utc = $end_utc = null;
                $duration_min = '?';
            }
            $result[] = array(
                'id'               => $r->id,
                'square_booking_id'=> $r->square_booking_id,
                'customer'         => $r->customer_name,
                'service'          => $r->service_name  ?: "id={$r->service_id}",
                'staff'            => $r->staff_name    ?: "id={$r->staff_id}",
                'status'           => $r->status,
                'square_synced'    => $r->square_synced,
                'start_time_raw'   => $r->start_time,
                'end_time_raw'     => $r->end_time,
                'duration_min'     => $duration_min,
                'start_local'      => $start_local ? $start_local->format('Y-m-d H:i:s T') : 'parse error',
                'end_local'        => $end_local   ? $end_local->format('Y-m-d H:i:s T')   : 'parse error',
                'start_utc'        => $start_utc   ? $start_utc->format('Y-m-d H:i:s').' UTC' : 'parse error',
                'end_utc'          => $end_utc     ? $end_utc->format('Y-m-d H:i:s').' UTC'   : 'parse error',
                'flag'             => ($duration_min !== '?' && ($duration_min <= 0 || $duration_min > 480))
                                        ? 'Warning: SUSPICIOUS DURATION'
                                        : (in_array($r->status, ['confirmed','pending']) ? 'BLOCKS AVAILABILITY' : 'OK not blocking'),
            );
        }

        wp_send_json_success(array(
            'date'          => $date,
            'timezone'      => $timezone,
            'booking_count' => count($result),
            'bookings'      => $result,
            'note'          => 'start_time/end_time in DB are stored in local timezone (' . $timezone . '). If any end_time looks wrong or duration is 0 or >480min, that record is causing the conflict.',
        ));
    }

    /**
     * Admin diagnostic: test Square availability search for a service
     */
    public function diagnose_service() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        check_ajax_referer('yatoon_admin_nonce', 'nonce');

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

        global $wpdb;
        $service = yatoon_db_get_row($wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}yatoon_services WHERE id = %d", $service_id
        ));
        if (!$service) wp_send_json_error('Service not found');

        $options = yatoon_db_get_results($wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}yatoon_service_options WHERE service_id = %d ORDER BY sort_order ASC, id ASC", $service_id
        ));

        $result = array(
            'service'             => array(
                'id'        => $service->id,
                'name'      => $service->name,
                'square_id' => $service->square_id,
                'price'     => $service->price,
            ),
            'variations'          => array(),
            'availability_tests'  => array(),
            'catalog_object'      => null,
            'correct_ids_found'   => array(),
        );

        foreach ($options as $opt) {
            $result['variations'][] = array(
                'id'             => $opt->id,
                'name'           => $opt->name,
                'square_id'      => $opt->square_id,
                'price_modifier' => $opt->price_modifier,
                'duration'       => $opt->duration_minutes,
            );
        }

        $tz       = new DateTimeZone('America/Indiana/Indianapolis');
        $tomorrow = new DateTime('tomorrow', $tz);
        $start    = (clone $tomorrow)->setTime(0,0,0)->format('c');
        $end      = (clone $tomorrow)->setTime(23,59,59)->format('c');

        foreach ($options as $opt) {
            if (empty($opt->square_id)) continue;
            $resp = $this->square_api->search_availability($opt->square_id, $start, $end, null);
            $test = array(
                'variation_name'  => $opt->name,
                'variation_sq_id' => $opt->square_id,
                'date_tested'     => $tomorrow->format('Y-m-d'),
            );
            if (is_wp_error($resp)) {
                $test['error'] = $resp->get_error_message();
            } else {
                $test['slots_found'] = isset($resp['availabilities']) ? count($resp['availabilities']) : 0;
                if (isset($resp['errors'])) $test['sq_errors'] = $resp['errors'];
            }
            $result['availability_tests'][] = $test;
        }

        // Fetch catalog object to see ALL its variation IDs and test each one
        $cat_resp = $this->square_api->get_catalog_object($service->square_id);
        if (!is_wp_error($cat_resp) && isset($cat_resp['object'])) {
            $obj = $cat_resp['object'];
            $variations_from_catalog = array();
            if (isset($obj['item_data']['variations'])) {
                foreach ($obj['item_data']['variations'] as $v) {
                    $vname = isset($v['item_variation_data']['name']) ? $v['item_variation_data']['name'] : '?';
                    $vprice = isset($v['item_variation_data']['price_money']['amount']) ? $v['item_variation_data']['price_money']['amount']/100 : 0;
                    $variations_from_catalog[] = array(
                        'id'    => $v['id'],
                        'name'  => $vname,
                        'price' => $vprice,
                    );
                    // Test this ID against Appointments API
                    $avail = $this->square_api->search_availability($v['id'], $start, $end, null);
                    if (!is_wp_error($avail) && isset($avail['availabilities']) && count($avail['availabilities']) > 0) {
                        $result['correct_ids_found'][] = array(
                            'variation_name' => $vname,
                            'correct_sq_id'  => $v['id'],
                            'slots'          => count($avail['availabilities']),
                        );
                    }
                }
            }
            $result['catalog_object'] = array(
                'type'       => $obj['type'],
                'variations' => $variations_from_catalog,
            );
        } else if (is_wp_error($cat_resp)) {
            $result['catalog_error'] = $cat_resp->get_error_message();
        }

        wp_send_json_success($result);
    }

    /**
     * Admin: Auto-fix incorrect variation IDs in the database
     * Called after diagnose finds correct_ids_found
     */
    public function fix_variation_ids() {
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        check_ajax_referer('yatoon_admin_nonce', 'nonce');

        $service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
        $fixes_raw  = isset($_POST['fixes']) ? stripslashes($_POST['fixes']) : '';
        $fixes      = json_decode($fixes_raw, true);

        if (!$service_id || empty($fixes)) {
            wp_send_json_error('Missing parameters');
        }

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

        // Get all stored options for this service
        $stored = yatoon_db_get_results($wpdb->prepare(
            "SELECT * FROM $table WHERE service_id = %d ORDER BY id ASC", $service_id
        ));

        $updated = 0;
        // Match by position order: fix[0] -> stored[0], fix[1] -> stored[1] etc.
        foreach ($fixes as $i => $fix) {
            if (!isset($stored[$i])) continue;
            $opt = $stored[$i];
            $new_sq_id = sanitize_text_field($fix['correct_sq_id']);
            $result = yatoon_db_update(
                $table,
                array('square_id' => $new_sq_id),
                array('id' => $opt->id),
                array('%s'),
                array('%d')
            );
            if ($result !== false) $updated++;
        }

        wp_send_json_success(array('updated' => $updated));
    }
}

// -------------------------------------------------------------
// Additional AJAX handlers (hooked via add_action in admin class)
// -------------------------------------------------------------

/**
 * AJAX: Google Calendar OAuth callback
 */
add_action('wp_ajax_yatoon_google_cal_callback', function() {
    if (!current_user_can('manage_options')) wp_die('Permission denied');
    if ( ! class_exists( 'YATOON_License' ) || ! YATOON_License::is_pro() ) {
        wp_die('Google Calendar sync requires an active Pro license.');
    }
    $state = sanitize_text_field($_GET['state'] ?? '');
    if ( empty($state) || !YATOON_Google_Calendar::verify_oauth_state($state) ) {
        wp_die('Invalid OAuth state.');
    }

    $code = sanitize_text_field($_GET['code'] ?? '');
    if (empty($code)) {
        wp_die('No authorization code received from Google.');
    }

    $result = YATOON_Google_Calendar::exchange_code($code);
    if ($result) {
        wp_safe_redirect(admin_url('admin.php?page=yatoon-booking-system&tab=integrations&google_connected=1'));
    } else {
        wp_safe_redirect(admin_url('admin.php?page=yatoon-booking-system&tab=integrations&google_error=1'));
    }
    exit;
});

/**
 * AJAX: Create / update a staff member (manual CRUD)
 */
add_action('wp_ajax_yatoon_save_staff', function() {
    if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');
    check_ajax_referer('yatoon_admin_nonce', 'nonce');

    global $wpdb;
    $table = $wpdb->prefix . 'yatoon_staff';
    static $staff_columns = null;

    $id        = intval($_POST['staff_id'] ?? 0);
    $name      = sanitize_text_field($_POST['name'] ?? '');
    $email     = sanitize_email($_POST['email'] ?? '');
    $bio       = sanitize_textarea_field($_POST['bio'] ?? '');
    $image_url = isset($_POST['image_url']) ? esc_url_raw(wp_unslash($_POST['image_url'])) : '';
    $calendar_color = isset($_POST['calendar_color']) ? sanitize_hex_color(wp_unslash($_POST['calendar_color'])) : '';
    if (!$calendar_color) {
        $calendar_color = '';
    }
    $is_active = intval($_POST['is_active'] ?? 1);
    $is_bookable = intval($_POST['is_bookable'] ?? 1);
    $time_off_rules = isset($_POST['time_off_rules']) ? wp_unslash($_POST['time_off_rules']) : '';
    $portal_role = isset($_POST['portal_role']) ? sanitize_key(wp_unslash($_POST['portal_role'])) : 'staff';
    if (!in_array($portal_role, array('staff', 'manager', 'owner'), true)) {
        $portal_role = 'staff';
    }
    $portal_permissions_raw = isset($_POST['portal_permissions']) ? sanitize_text_field(wp_unslash($_POST['portal_permissions'])) : 'view_schedule';
    $portal_permissions = function_exists('yatoon_normalize_staff_portal_permissions')
        ? yatoon_normalize_staff_portal_permissions($portal_permissions_raw)
        : 'view_schedule';

    if (empty($name)) {
        wp_send_json_error(array('message' => 'Staff name is required.'));
    }

    if ($staff_columns === null) {
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $columns = yatoon_db_get_col("DESC $table", 0);
        $staff_columns = is_array($columns) ? $columns : array();
    }

    $data = array(
        'name'        => $name,
        'email'       => $email,
        'bio'         => $bio,
        'is_active'   => $is_active,
        'is_bookable' => $is_bookable,
    );

    $formats = array('%s','%s','%s','%d','%d');
    if (in_array('image_url', $staff_columns, true)) {
        $data['image_url'] = $image_url;
        $formats[] = '%s';
    }

    if (in_array('calendar_color', $staff_columns, true)) {
        $data['calendar_color'] = $calendar_color;
        $formats[] = '%s';
    }

    if (in_array('portal_permissions', $staff_columns, true)) {
        $data['portal_permissions'] = $portal_permissions;
        $formats[] = '%s';
    }
    if (in_array('portal_role', $staff_columns, true)) {
        $data['portal_role'] = $portal_role;
        $formats[] = '%s';
    }

    if ($id > 0) {
        $result = yatoon_db_update($table, $data, array('id' => $id), $formats, array('%d'));
        if (class_exists('YATOON_Business_Hours')) {
            update_option(
                YATOON_Business_Hours::get_staff_time_off_option_key($id),
                YATOON_Business_Hours::normalize_staff_time_off_rules($time_off_rules)
            );
        }
        wp_send_json_success(array('message' => 'Staff updated.', 'id' => $id));
    } else {
        // New staff member
        $current_staff_count = (int) yatoon_db_get_var( "SELECT COUNT(*) FROM $table" );
        if ( class_exists( 'YATOON_License' ) && ! YATOON_License::check_limit( 'staff', $current_staff_count + 1 ) ) {
            wp_send_json_error( array(
                'message' => 'Unlimited staff requires an active Pro license.',
            ) );
        }

        $manual_external_id = 'manual_' . wp_generate_password(12, false, false);

        if (in_array('square_id', $staff_columns, true)) {
            $data['square_id'] = $manual_external_id;
        }

        if (in_array('square_staff_id', $staff_columns, true)) {
            $data['square_staff_id'] = $manual_external_id;
        }

        $result = yatoon_db_insert($table, $data);
        if ($result === false) {
            wp_send_json_error(array('message' => 'Database error: ' . $wpdb->last_error));
        }
        if (class_exists('YATOON_Business_Hours')) {
            update_option(
                YATOON_Business_Hours::get_staff_time_off_option_key($wpdb->insert_id),
                YATOON_Business_Hours::normalize_staff_time_off_rules($time_off_rules)
            );
        }
        wp_send_json_success(array('message' => 'Staff member added.', 'id' => $wpdb->insert_id));
    }
});

/**
 * AJAX: Delete a staff member
 */
add_action('wp_ajax_yatoon_delete_staff', function() {
    if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');
    check_ajax_referer('yatoon_admin_nonce', 'nonce');

    global $wpdb;
    $staff_ids = array();
    if (isset($_POST['staff_ids']) && is_array($_POST['staff_ids'])) {
        $staff_ids = array_map('intval', wp_unslash($_POST['staff_ids']));
    } elseif (isset($_POST['staff_id'])) {
        $staff_ids = array(intval($_POST['staff_id']));
    }

    $staff_ids = array_values(array_unique(array_filter($staff_ids)));
    if (empty($staff_ids)) wp_send_json_error(array('message' => 'Invalid staff ID'));

    $deleted = 0;
    foreach ($staff_ids as $id) {
        yatoon_db_delete($wpdb->prefix . 'yatoon_staff_services', array('staff_id' => $id), array('%d'));
        yatoon_db_delete($wpdb->prefix . 'yatoon_staff_time_off_requests', array('staff_id' => $id), array('%d'));
        $result = yatoon_db_delete($wpdb->prefix . 'yatoon_staff', array('id' => $id), array('%d'));
        if (class_exists('YATOON_Business_Hours')) {
            delete_option(YATOON_Business_Hours::get_staff_time_off_option_key($id));
        }
        if ($result) {
            $deleted++;
        }
    }

    if ($deleted > 0) {
        wp_send_json_success(array(
            'message' => sprintf(
                /* translators: %d: number of deleted staff members */
                _n('%d staff member deleted.', '%d staff members deleted.', $deleted, 'yatoon-booking-system'),
                $deleted
            ),
            'deleted' => $deleted,
        ));
    }

    wp_send_json_error(array('message' => 'No staff members were deleted.'));
});

/**
 * AJAX: Get single staff member for editing
 */
add_action('wp_ajax_yatoon_get_staff_member', function() {
    if (!current_user_can('manage_options')) wp_send_json_error('Permission denied');
    check_ajax_referer('yatoon_admin_nonce', 'nonce');

    global $wpdb;
    $id = intval($_GET['staff_id'] ?? 0);
    if (!$id) wp_send_json_error('Invalid ID');

    $member = yatoon_db_get_row($wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}yatoon_staff WHERE id = %d",
        $id
    ));

    if (!$member) wp_send_json_error('Not found');
    wp_send_json_success(array('staff' => $member));
});

/**
 * AJAX: Register new settings for integrations
 */
add_action('admin_init', function() {
    // Twilio
    register_setting('yatoon_settings', 'yatoon_twilio_enabled', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_account_sid', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_auth_token', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_from_number', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_admin_number', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_customer_sms', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_admin_sms', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_reminder_sms', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_customer_template', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_admin_template', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_twilio_reminder_template', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    // Google Calendar
    register_setting('yatoon_settings', 'yatoon_google_cal_enabled', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_google_cal_client_id', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_google_cal_client_secret', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_google_cal_refresh_token', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_google_cal_calendar_id', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_google_cal_send_invites', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    // Email template
    register_setting('yatoon_settings', 'yatoon_email_template_style', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_email_notify_admin', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    // Extra brand colors
    register_setting('yatoon_brand_settings', 'yatoon_accent_color', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_brand_settings', 'sbs_button_radius', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_brand_settings', 'sbs_card_radius', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_brand_settings', 'sbs_font_family', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_brand_settings', 'sbs_readmore_color', array('sanitize_callback' => 'sanitize_hex_color'));
    register_setting('yatoon_brand_settings', 'sbs_heading_font', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_brand_settings', 'sbs_heading_color', array('sanitize_callback' => 'sanitize_hex_color'));
    register_setting('yatoon_brand_settings', 'sbs_heading_size', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_brand_settings', 'sbs_main_title_size', array('sanitize_callback' => 'yatoon_sanitize_setting'));
}, 20); // priority 20 so it runs after the main admin class

// Phone & Verification settings registration
add_action('admin_init', function() {
    register_setting('yatoon_settings', 'yatoon_phone_required',       array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_phone_mode',           array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_phone_preset_country', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_verify_email_otp',     array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_verify_sms_otp',       array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_otp_expiry_minutes',   array('sanitize_callback' => 'yatoon_sanitize_setting'));
}, 25);

function yatoon_staff_portal_permission_keys() {
    return array(
        'view_schedule',
        'view_all_schedules',
        'update_status',
        'create_booking',
        'edit_booking',
        'cancel_booking',
        'view_clients',
        'manage_clients',
        'send_messages',
        'checkout',
        'request_time_off',
        'approve_time_off',
    );
}

function yatoon_staff_portal_role_permissions($role) {
    $staff = array(
        'view_schedule',
        'update_status',
        'view_clients',
        'send_messages',
        'request_time_off',
    );
    $manager = array_merge($staff, array(
        'view_all_schedules',
        'create_booking',
        'edit_booking',
        'cancel_booking',
        'manage_clients',
        'checkout',
        'approve_time_off',
    ));

    if ($role === 'owner') {
        return yatoon_staff_portal_permission_keys();
    }
    return $role === 'manager' ? array_values(array_unique($manager)) : $staff;
}

function yatoon_normalize_staff_portal_permissions($raw) {
    $allowed = yatoon_staff_portal_permission_keys();
    $parts   = is_array($raw) ? $raw : explode(',', (string) $raw);
    $clean   = array();

    foreach ($parts as $part) {
        $key = sanitize_key(trim((string) $part));
        if (in_array($key, $allowed, true)) {
            $clean[] = $key;
        }
    }

    $clean[] = 'view_schedule';
    $clean   = array_values(array_unique($clean));

    return implode(',', $clean);
}

function yatoon_staff_portal_permissions_migration() {
    global $wpdb;
    $table = $wpdb->prefix . 'yatoon_staff';

    $exists = yatoon_db_get_var("SHOW TABLES LIKE '{$table}'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if (!$exists) {
        return;
    }

    $has_permissions = yatoon_db_get_var("SHOW COLUMNS FROM {$table} LIKE 'portal_permissions'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if (!$has_permissions) {
        yatoon_db_query("ALTER TABLE {$table} ADD COLUMN portal_permissions varchar(255) DEFAULT 'view_schedule,update_status,view_clients,send_messages,request_time_off' AFTER sort_order"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    }
    $has_role = yatoon_db_get_var("SHOW COLUMNS FROM {$table} LIKE 'portal_role'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if (!$has_role) {
        yatoon_db_query("ALTER TABLE {$table} ADD COLUMN portal_role varchar(20) DEFAULT 'staff' AFTER sort_order"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    }
    $has_calendar_color = yatoon_db_get_var("SHOW COLUMNS FROM {$table} LIKE 'calendar_color'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if (!$has_calendar_color) {
        yatoon_db_query("ALTER TABLE {$table} ADD COLUMN calendar_color varchar(20) DEFAULT '' AFTER image_url"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    }
}
add_action('plugins_loaded', 'yatoon_staff_portal_permissions_migration', 8);

/**
 * AJAX: Send OTP (email or SMS) for booking verification
 */
add_action('wp_ajax_yatoon_send_otp',        'yatoon_send_otp_handler');
add_action('wp_ajax_nopriv_yatoon_send_otp', 'yatoon_send_otp_handler');
if ( ! function_exists( 'yatoon_public_rate_limit' ) ) {
    function yatoon_public_rate_limit( $scope, $limit = 5, $minutes = 10, $contact = '' ) {
        $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        $parts = array( sanitize_key( $scope ) );
        if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            $parts[] = $ip;
        }
        if ( $contact !== '' ) {
            $parts[] = strtolower( sanitize_text_field( $contact ) );
        }

        $key = 'yatoon_public_' . md5( implode( '|', $parts ) );
        $count = (int) get_transient( $key );
        if ( $count >= $limit ) {
            wp_send_json_error( array(
                'message' => sprintf(
                    __( 'Too many requests. Please wait %d minutes and try again.', 'yatoon-booking-system' ),
                    $minutes
                ),
            ) );
        }

        set_transient( $key, $count + 1, $minutes * MINUTE_IN_SECONDS );
    }
}

function yatoon_send_otp_handler() {
    check_ajax_referer('yatoon_frontend_nonce', 'nonce');

    $channel = isset($_POST['channel']) ? sanitize_text_field(wp_unslash($_POST['channel'])) : 'email';
    $email   = isset($_POST['email'])   ? sanitize_email(wp_unslash($_POST['email']))           : '';
    $phone   = isset($_POST['phone'])   ? sanitize_text_field(wp_unslash($_POST['phone']))       : '';
    $name    = isset($_POST['name'])    ? sanitize_text_field(wp_unslash($_POST['name']))        : '';
    yatoon_public_rate_limit( 'otp_send', 3, 15, $email ?: $phone );

    if ($channel === 'email' && empty($email)) {
        wp_send_json_error(array('message' => __('Email address is required to send OTP.', 'yatoon-booking-system')));
    }
    if ($channel === 'sms' && empty($phone)) {
        wp_send_json_error(array('message' => __('Phone number is required to send OTP.', 'yatoon-booking-system')));
    }

    // Generate 6-digit OTP
    $otp     = str_pad((string) wp_rand(0, 999999), 6, '0', STR_PAD_LEFT);
    $expiry  = intval(get_option('yatoon_otp_expiry_minutes', 10));
    $expires = time() + ($expiry * 60);
    $key     = md5($channel . $email . $phone);

    // Store transiently (per-channel, per-contact)
    set_transient('yatoon_otp_' . $key, array('otp' => $otp, 'expires' => $expires), $expiry * 60 + 30);

    $biz = get_option('yatoon_business_name', get_bloginfo('name'));

    if ($channel === 'email') {
        $subject = sprintf(__('Your booking code for %s', 'yatoon-booking-system'), $biz);
        $body    = sprintf(
            __("Hi %s,

Your verification code is: %s

This code expires in %d minutes.

%s", 'yatoon-booking-system'),
            $name ?: __('there', 'yatoon-booking-system'), $otp, $expiry, $biz
        );
        $sent = wp_mail($email, $subject, $body);
        if (!$sent) {
            wp_send_json_error(array('message' => __('Failed to send verification email. Please try again.', 'yatoon-booking-system')));
        }
        wp_send_json_success(array('message' => sprintf(__('A 6-digit code was sent to %s', 'yatoon-booking-system'), $email)));
    } else {
        // SMS via Twilio
        if (!get_option('yatoon_twilio_enabled') || !get_option('yatoon_twilio_account_sid')) {
            wp_send_json_error(array('message' => __('SMS verification requires Twilio to be configured in Settings -> SMS.', 'yatoon-booking-system')));
        }
        $sid    = get_option('yatoon_twilio_account_sid');
        $token  = YATOON_Crypto::get_option('yatoon_twilio_auth_token');
        $from   = get_option('yatoon_twilio_from_number');
        $msg    = sprintf(__('Your %s booking code: %s (expires in %d min)', 'yatoon-booking-system'), $biz, $otp, $expiry);
        $url    = "https://api.twilio.com/2010-04-01/Accounts/{$sid}/Messages.json";
        $resp   = wp_remote_post($url, array(
            'headers' => array('Authorization' => 'Basic ' . base64_encode("{$sid}:{$token}")),
            'body'    => array('From' => $from, 'To' => $phone, 'Body' => $msg),
        ));
        if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) >= 400) {
            wp_send_json_error(array('message' => __('Failed to send SMS. Please check Twilio settings.', 'yatoon-booking-system')));
        }
        wp_send_json_success(array('message' => sprintf(__('A 6-digit code was sent via SMS to %s', 'yatoon-booking-system'), $phone)));
    }
}

/**
 * AJAX: Verify OTP entered by customer
 */
add_action('wp_ajax_yatoon_verify_otp',        'yatoon_verify_otp_handler');
add_action('wp_ajax_nopriv_yatoon_verify_otp', 'yatoon_verify_otp_handler');
function yatoon_verify_otp_handler() {
    check_ajax_referer('yatoon_frontend_nonce', 'nonce');

    $channel  = isset($_POST['channel']) ? sanitize_text_field(wp_unslash($_POST['channel'])) : 'email';
    $email    = isset($_POST['email'])   ? sanitize_email(wp_unslash($_POST['email']))         : '';
    $phone    = isset($_POST['phone'])   ? sanitize_text_field(wp_unslash($_POST['phone']))     : '';
    $code     = isset($_POST['code'])    ? sanitize_text_field(wp_unslash($_POST['code']))      : '';
    yatoon_public_rate_limit( 'otp_verify', 8, 15, $email ?: $phone );

    $key      = md5($channel . $email . $phone);
    $stored   = get_transient('yatoon_otp_' . $key);

    if (!$stored || !is_array($stored)) {
        wp_send_json_error(array('message' => __('Verification code expired or not found. Please request a new code.', 'yatoon-booking-system')));
    }
    if (time() > $stored['expires']) {
        delete_transient('yatoon_otp_' . $key);
        wp_send_json_error(array('message' => __('Verification code has expired. Please request a new one.', 'yatoon-booking-system')));
    }
    if (!hash_equals($stored['otp'], $code)) {
        wp_send_json_error(array('message' => __('Incorrect code. Please try again.', 'yatoon-booking-system')));
    }

    // OTP is correct - mark as verified in session-safe transient keyed to browser session
    $verify_key = md5($channel . $email . $phone . 'verified');
    set_transient('yatoon_otp_verified_' . $verify_key, 1, 3600); // valid 1 hour
    delete_transient('yatoon_otp_' . $key);

    wp_send_json_success(array('message' => __('Verified!', 'yatoon-booking-system'), 'verified_key' => $verify_key));
}

/**
 * AJAX: Bulk-fetch "next available opening" for multiple services
 * Lightweight check: searches forward up to 14 days, returns first day with at least one open slot.
 * Cached per-service for 5 minutes via transient.
 *
 * Request:  POST service_ids[] = array of service IDs
 * Response: { service_id: { label: "Today 3:00 pm", urgent: bool } | null }
 */
add_action('wp_ajax_yatoon_get_next_openings',        'yatoon_get_next_openings_handler');
add_action('wp_ajax_nopriv_yatoon_get_next_openings', 'yatoon_get_next_openings_handler');
function yatoon_get_next_openings_handler() {
    check_ajax_referer('yatoon_frontend_nonce', 'nonce');

    if (!class_exists('YATOON_Business_Hours')) {
        wp_send_json_success(array('openings' => array()));
    }

    $service_ids = isset($_POST['service_ids']) ? (array) $_POST['service_ids'] : array();
    $service_ids = array_slice(array_values(array_unique(array_filter(array_map('intval', $service_ids)))), 0, 24);
    if (empty($service_ids)) {
        wp_send_json_success(array('openings' => array()));
    }

    // Don't run on Square/Vagaro - those need real API calls and are too slow for cards.
    // For non-local platforms we return empty (cards just won't show the opening hint).
    $platform = get_option('yatoon_active_platform', 'local');
    if ($platform !== 'local') {
        wp_send_json_success(array('openings' => array()));
    }

    $tz_str   = ( wp_timezone_string() ?: 'America/Chicago' );
    try { $tz = new DateTimeZone($tz_str); } catch (Exception $e) { $tz = new DateTimeZone('UTC'); }
    $now      = new DateTime('now', $tz);
    $today_ts = (int) $now->format('U');

    $openings = array();
    $batch_key_ids = $service_ids;
    sort($batch_key_ids);
    $batch_cache_key = 'yatoon_next_open_batch_' . md5(implode(',', $batch_key_ids) . '|' . $tz_str . '|' . YATOON_VERSION);
    $batch_cached = get_transient($batch_cache_key);
    if (is_array($batch_cached)) {
        wp_send_json_success(array('openings' => $batch_cached, 'cached' => true));
    }

    foreach ($service_ids as $svc_id) {
        $cache_key = 'yatoon_next_open_' . md5($svc_id . '|' . $tz_str . '|' . YATOON_VERSION);
        $cached    = get_transient($cache_key);
        if ($cached !== false) {
            $openings[$svc_id] = $cached;
            continue;
        }

        $found_label = null;
        $found_urgent = false;

        // Search forward up to 14 days
        for ($d = 0; $d < 14; $d++) {
            $check = clone $now;
            $check->modify('+' . $d . ' days');
            $date_str = $check->format('Y-m-d');

            $slots = YATOON_Business_Hours::generate_slots($date_str, $svc_id, 0);
            if (!is_array($slots) || empty($slots)) continue;

            // Find first slot that's still in the future (today's slots may have already passed)
            foreach ($slots as $slot) {
                // generate_slots returns array of times like '09:00' or full datetime objects depending on impl
                $slot_time = is_array($slot) ? ($slot['time'] ?? $slot['start'] ?? '') : $slot;
                if (!$slot_time) continue;

                // Build a DateTime for slot
                if (strpos($slot_time, 'T') !== false || strpos($slot_time, ' ') !== false) {
                    try { $slot_dt = new DateTime($slot_time, $tz); } catch (Exception $e) { continue; }
                } else {
                    try { $slot_dt = new DateTime($date_str . ' ' . $slot_time, $tz); } catch (Exception $e) { continue; }
                }

                if ($slot_dt->getTimestamp() <= $today_ts + 1800) continue; // need at least 30 min lead time

                $time_label = $slot_dt->format('g:i a');
                if ($d === 0) {
                    $found_label  = sprintf(__('Today %s', 'yatoon-booking-system'), $time_label);
                    $found_urgent = true;
                } elseif ($d === 1) {
                    $found_label = sprintf(__('Tomorrow %s', 'yatoon-booking-system'), $time_label);
                } else {
                    $found_label = $slot_dt->format('D, M j ' . 'g:i a');
                }
                break 2;
            }
        }

        $result = $found_label ? array('label' => $found_label, 'urgent' => $found_urgent) : null;
        set_transient($cache_key, $result, 5 * MINUTE_IN_SECONDS);
        $openings[$svc_id] = $result;
    }

    set_transient($batch_cache_key, $openings, 5 * MINUTE_IN_SECONDS);
    wp_send_json_success(array('openings' => $openings));
}

// Register the new option
add_action('admin_init', function() {
    register_setting('yatoon_settings', 'yatoon_show_ratings_on_cards', array('sanitize_callback' => 'yatoon_sanitize_setting'));
    register_setting('yatoon_settings', 'yatoon_show_next_opening',     array('sanitize_callback' => 'yatoon_sanitize_setting'));
}, 26);
// phpcs:enable

// -- Staff Portal PIN management ------------------------------------------------
add_action('wp_ajax_yatoon_set_staff_pin', function() {
    check_ajax_referer('yatoon_set_staff_pin', 'nonce');
    if (!current_user_can('manage_options')) wp_send_json_error(array('message' => 'Permission denied.'));

    global $wpdb;
    $staff_id = absint($_POST['staff_id'] ?? 0);
    $pin      = preg_replace('/\D/', '', sanitize_text_field( wp_unslash( $_POST['pin'] ?? '' ) ) );

    if (!$staff_id) wp_send_json_error(array('message' => 'Invalid staff ID.'));

    // Verify staff exists
    $exists = yatoon_db_get_var($wpdb->prepare(
        "SELECT id FROM {$wpdb->prefix}yatoon_staff WHERE id = %d LIMIT 1", $staff_id
    )); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if (!$exists) wp_send_json_error(array('message' => 'Staff not found.'));

    // Add portal_pin column if not present
    $cols = yatoon_db_get_col("DESCRIBE {$wpdb->prefix}yatoon_staff", 0); // phpcs:ignore
    if (!in_array('portal_pin', $cols, true)) {
        yatoon_db_query("ALTER TABLE {$wpdb->prefix}yatoon_staff ADD COLUMN `portal_pin` varchar(255) DEFAULT '' AFTER `sort_order`"); // phpcs:ignore
    }

    if ($pin === '') {
        // Remove PIN
        yatoon_db_update($wpdb->prefix . 'yatoon_staff', array('portal_pin' => ''), array('id' => $staff_id), array('%s'), array('%d')); // phpcs:ignore
        wp_send_json_success(array('message' => 'PIN removed.'));
    }

    $pin_length = in_array( (int) get_option( 'yatoon_staff_pin_length', 4 ), array( 4, 6 ), true ) ? (int) get_option( 'yatoon_staff_pin_length', 4 ) : 4;
    if (strlen($pin) !== $pin_length) wp_send_json_error(array('message' => sprintf('PIN must be %d digits.', $pin_length)));

    $hashed = wp_hash_password($pin);
    yatoon_db_update($wpdb->prefix . 'yatoon_staff', array('portal_pin' => $hashed), array('id' => $staff_id), array('%s'), array('%d')); // phpcs:ignore
    wp_send_json_success(array('message' => 'PIN saved.'));
});

// -- Save Square API settings directly --------------------------------------
add_action('wp_ajax_yatoon_save_square_api_settings', function() {
    check_ajax_referer('yatoon_save_square_api', 'nonce');
    if (!current_user_can('manage_options')) wp_send_json_error(array('message' => 'Permission denied.'));

    $token = sanitize_text_field(wp_unslash($_POST['access_token'] ?? ''));
    $loc   = sanitize_text_field(wp_unslash($_POST['location_id']  ?? ''));
    $env   = sanitize_text_field(wp_unslash($_POST['environment']   ?? 'production'));

    if ($token) YATOON_Crypto::update_option('yatoon_square_access_token', $token);
    if ($loc)   update_option('yatoon_square_location_id',  $loc);
    update_option('yatoon_square_environment', in_array($env, array('production','sandbox'), true) ? $env : 'production');

    wp_send_json_success(array('message' => 'Square API settings saved.'));
});

// -- Save ALL settings via AJAX ----------------------------------------------
// This bypasses options.php which can be blocked by Hostinger WAF
add_action('wp_ajax_yatoon_save_all_settings', function() {
    check_ajax_referer('yatoon_save_all_settings', 'nonce');
    if (!current_user_can('manage_options')) {
        wp_send_json_error(array('message' => 'Permission denied.'));
    }

    // Per-tab field definitions - only save fields belonging to the current tab
    $tab_fields = array(
        'general' => array(
            'yatoon_active_platform', 'yatoon_booking_template', 'yatoon_show_advanced_menu', 'yatoon_test_booking_mode', 'yatoon_square_environment', 'sbs_square_environment',
            'yatoon_square_access_token', 'yatoon_square_location_id',
            'yatoon_vagaro_api_key', 'yatoon_vagaro_business_id',
            'yatoon_auto_sync_enabled', 'yatoon_sync_interval',
            'yatoon_booking_sync_enabled', 'yatoon_booking_sync_interval',
            'yatoon_phone_mode', 'yatoon_phone_required', 'yatoon_phone_preset_country',
            'yatoon_verify_email_otp', 'yatoon_verify_sms_otp', 'yatoon_otp_expiry_minutes',
            'yatoon_staff_pin_length', 'yatoon_staff_idle_lock_minutes',
            'yatoon_policy_enabled', 'yatoon_policy_require_ack',
            'yatoon_policy_title', 'yatoon_policy_text', 'yatoon_policy_ack_label',
            'yatoon_consent_enabled', 'yatoon_consent_title', 'yatoon_consent_text',
            'yatoon_consent_signature_label',
            'yatoon_waitlist_enabled', 'yatoon_waitlist_auto_notify',
            'yatoon_multi_appointment', 'yatoon_max_guests_together',
            'yatoon_multi_location_enabled', 'yatoon_booking_immersive_mode',
            'yatoon_show_next_opening', 'yatoon_show_availability_source_note',
            'yatoon_availability_help_text', 'yatoon_latest_end_hour',
            'yatoon_show_success_next', 'yatoon_success_next_title',
            'yatoon_success_action_note', 'yatoon_success_followup_text',
            'yatoon_success_deposit_text', 'yatoon_success_pay_later_text',
            'yatoon_rebooking_nudge_text', 'yatoon_rebooking_nudge_btn',
            'yatoon_show_venue_hero', 'yatoon_venue_kicker',
            'yatoon_venue_deposit_label', 'yatoon_venue_pay_later_label',
            'yatoon_tip_enabled', 'yatoon_tip_label', 'yatoon_tip_presets',
            'yatoon_gift_card_enabled', 'yatoon_enable_coupons',
            'yatoon_show_portal_banner', 'yatoon_portal_banner_bg',
            'yatoon_portal_banner_text', 'yatoon_portal_banner_prompt',
            'yatoon_portal_url', 'yatoon_portal_label', 'yatoon_portal_new_tab',
            'yatoon_portal_login_title', 'yatoon_portal_login_subtitle',
            'yatoon_portal_login_brand', 'yatoon_portal_btn_label',
            'yatoon_portal_primary_color', 'yatoon_book_new_url',
            'yatoon_show_ratings_on_cards', 'yatoon_public_rating_label',
            'yatoon_payment_request_url', 'yatoon_square_checkout_url',
            'yatoon_square_app_url', 'yatoon_square_checkout_note',
            'yatoon_square_payments_enabled', 'yatoon_square_application_id',
            'yatoon_square_payment_mode', 'yatoon_square_payment_methods',
            'yatoon_square_country_code',
            'yatoon_deposit_enabled',
            'yatoon_deposit_amount', 'yatoon_deposit_type',
            'yatoon_deposit_stripe_key', 'yatoon_deposit_stripe_secret',
            'yatoon_stripe_webhook_secret',
            'yatoon_whatsapp_enabled', 'yatoon_whatsapp_number',
            'yatoon_whatsapp_prefill', 'yatoon_whatsapp_label',
            'yatoon_text_us_enabled', 'yatoon_textus_bg_color', 'yatoon_textus_text_color',
            'yatoon_pwa_enabled', 'yatoon_banner_bg_color',
            'yatoon_banner_btn_color', 'yatoon_banner_text_color',
            'yatoon_contact_phone', 'yatoon_contact_email',
            'yatoon_business_address', 'yatoon_business_phone', 'yatoon_business_website',
        ),
        'hours' => array(
            'sbs_header_offset', 'sbs_slot_interval', 'yatoon_header_offset', 'yatoon_slot_interval',
            'yatoon_booking_immersive_mode', 'yatoon_latest_end_hour',
            'yatoon_max_guests_together', 'yatoon_multi_appointment',
        ),
        'email' => 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',
        ),
        'sms' => 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',
        ),
        'calendar' => 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',
        ),
        'colors' => array(
            'yatoon_business_name', 'yatoon_business_logo', 'yatoon_business_logo_id',
            'sbs_business_logo', 'sbs_business_logo_id',
            'yatoon_primary_color', 'yatoon_secondary_color', 'yatoon_body_text_color',
            'yatoon_price_color', 'yatoon_button_radius', 'yatoon_card_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',
        ),
    );

    // Get current tab from POST
    $active_tab = sanitize_key( $_POST['active_tab'] ?? 'general' );
    // Use tab-specific fields if available, otherwise use all allowed fields
    $tab_allowed = isset( $tab_fields[ $active_tab ] ) ? $tab_fields[ $active_tab ] : null;

    // All allowed option keys - complete list from settings.php
    $allowed = array(
        // Square / Platform
        'yatoon_active_platform', 'yatoon_booking_template', 'yatoon_show_advanced_menu', 'yatoon_test_booking_mode', 'yatoon_square_environment', 'sbs_square_environment',
        'yatoon_square_access_token', 'yatoon_square_location_id',
        'yatoon_vagaro_api_key', 'yatoon_vagaro_business_id',
        // Sync
        'yatoon_auto_sync_enabled', 'yatoon_sync_interval',
        'yatoon_booking_sync_enabled', 'yatoon_booking_sync_interval',
        // Email
        'yatoon_send_confirmation_email', 'yatoon_email_notify_admin',
        'yatoon_email_subject', 'yatoon_email_template', 'yatoon_email_template_style',
        // Manage booking
        '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',
        // Reminder / Review
        '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',
        // No-show followup
        'yatoon_no_show_followup_enabled', 'yatoon_no_show_followup_hours_after',
        'yatoon_no_show_followup_subject', 'yatoon_no_show_followup_template',
        // Cancel / Reschedule emails
        'yatoon_cancel_subject', 'yatoon_cancel_template',
        'yatoon_reschedule_subject', 'yatoon_reschedule_template',
        // Business info
        'yatoon_business_name', 'yatoon_contact_phone', 'yatoon_contact_email',
        'yatoon_business_address', 'yatoon_business_phone', 'yatoon_business_website',
        'yatoon_business_logo', 'yatoon_business_logo_id',
        'sbs_business_address', 'sbs_business_logo', 'sbs_business_logo_id',
        'sbs_business_phone', 'sbs_business_website',
        // Brand & Colors
        'yatoon_primary_color', 'yatoon_secondary_color', 'yatoon_body_text_color',
        'yatoon_price_color', 'yatoon_portal_primary_color', 'yatoon_banner_bg_color',
        'yatoon_banner_btn_color', 'yatoon_banner_text_color',
        'yatoon_textus_bg_color', 'yatoon_textus_text_color',
        'yatoon_button_radius', 'yatoon_card_radius', 'yatoon_font_family',
        'yatoon_header_offset', 'yatoon_slot_interval',
        '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',
        'sbs_header_offset', 'sbs_slot_interval',
        // Checkout options
        'yatoon_tip_enabled', 'yatoon_tip_label', 'yatoon_tip_presets',
        'yatoon_gift_card_enabled', 'yatoon_enable_coupons',
        // Phone / OTP
        'yatoon_phone_mode', 'yatoon_phone_required', 'yatoon_phone_preset_country',
        'yatoon_verify_email_otp', 'yatoon_verify_sms_otp', 'yatoon_otp_expiry_minutes',
        // Staff portal security
        'yatoon_staff_pin_length', 'yatoon_staff_idle_lock_minutes',
        // Policy / Consent
        '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',
        // Waitlist
        'yatoon_waitlist_enabled', 'yatoon_waitlist_auto_notify',
        // Booking options
        '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_availability_help_text', 'yatoon_slot_interval',
        'yatoon_latest_end_hour',
        // Success page
        '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',
        // Portal
        '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_show_portal_banner', 'yatoon_portal_banner_bg',
        'yatoon_portal_banner_text', 'yatoon_portal_banner_prompt',
        'yatoon_book_new_url',
        // Venue / Hero
        'yatoon_show_venue_hero', 'yatoon_venue_kicker',
        'yatoon_venue_deposit_label', 'yatoon_venue_pay_later_label',
        // Reviews
        'yatoon_show_ratings_on_cards', 'yatoon_public_rating_label',
        // Deposit / Stripe
        '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_square_payments_enabled', 'yatoon_square_application_id',
        'yatoon_square_payment_mode', 'yatoon_square_payment_methods',
        'yatoon_square_country_code',
        // WhatsApp / Text Us
        'yatoon_whatsapp_enabled', 'yatoon_whatsapp_number', 'yatoon_whatsapp_prefill',
        'yatoon_whatsapp_label', 'yatoon_text_us_enabled',
        // Google Calendar
        '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',
        // Twilio / SMS
        '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',
        // PWA
        'yatoon_pwa_enabled',
    );

    // Keys that should be deleted (not saved as empty) so defaults kick in
    $template_keys = array(
        'yatoon_email_template', 'yatoon_email_subject',
        'yatoon_reminder_template', 'yatoon_reminder_subject',
        'yatoon_review_template', 'yatoon_review_subject',
        'yatoon_cancel_template', 'yatoon_cancel_subject',
        'yatoon_reschedule_template', 'yatoon_reschedule_subject',
        'yatoon_no_show_followup_template', 'yatoon_no_show_followup_subject',
        'yatoon_policy_text', 'yatoon_policy_title', 'yatoon_policy_ack_label',
        'yatoon_consent_text', 'yatoon_consent_title', 'yatoon_consent_signature_label',
        'yatoon_twilio_customer_template', 'yatoon_twilio_admin_template',
        'yatoon_twilio_reminder_template',
        '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', 'yatoon_venue_kicker',
        'yatoon_venue_pay_later_label', 'yatoon_venue_deposit_label',
        'yatoon_tip_label', 'yatoon_whatsapp_label', 'yatoon_whatsapp_prefill',
        'yatoon_portal_login_title', 'yatoon_portal_login_subtitle',
        'yatoon_portal_btn_label', 'yatoon_portal_banner_text',
    );

    // Fields that should NEVER be overwritten with empty (passwords, secrets, tokens, platform settings)
    $protected_keys = array(
        // Platform selection - only present in General tab
        'yatoon_active_platform', 'yatoon_booking_template', 'yatoon_show_advanced_menu', 'yatoon_test_booking_mode', 'yatoon_square_environment', 'sbs_square_environment',
        // API credentials
        'yatoon_square_access_token', 'yatoon_square_location_id',
        'yatoon_vagaro_api_key', 'yatoon_vagaro_business_id',
        // Payment
        'yatoon_deposit_stripe_key', 'yatoon_deposit_stripe_secret',
        'yatoon_stripe_webhook_secret',
        'yatoon_square_application_id',
        // SMS
        'yatoon_twilio_account_sid', 'yatoon_twilio_auth_token',
        'yatoon_twilio_from_number', 'yatoon_twilio_admin_number',
        // Google Calendar
        'yatoon_google_cal_client_id', 'yatoon_google_cal_client_secret',
        'yatoon_google_cal_refresh_token', 'yatoon_google_cal_calendar_id',
        // Sync settings
        'yatoon_booking_sync_interval', 'yatoon_sync_interval',
        // Business identity
        'yatoon_business_logo', 'yatoon_business_logo_id',
        'sbs_business_logo', 'sbs_business_logo_id',
    );

    $saved = 0;
    foreach ($allowed as $key) {
        // Skip fields not belonging to current tab
        if ( $tab_allowed !== null && ! in_array( $key, $tab_allowed, true ) ) {
            continue;
        }
        if (isset($_POST[$key])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
            $raw = wp_unslash($_POST[$key]);
            if (is_array($raw)) {
                $val = array_values(array_filter(array_map('sanitize_key', $raw)));
            } else {
                $val = sanitize_textarea_field($raw);
            }
            if ($key === 'yatoon_staff_pin_length') {
                $val = in_array((int) $val, array(4, 6), true) ? (string) (int) $val : '4';
            }

            // Never overwrite protected keys with empty value
            if ($val === '' && in_array($key, $protected_keys, true)) {
                continue; // skip - keep existing value
            }
            // For template/text fields: if empty, delete option so defaults are used
            if ($val === '' && in_array($key, $template_keys, true)) {
                delete_option($key);
            } elseif ( YATOON_Crypto::is_sensitive( $key ) ) {
                YATOON_Crypto::update_option($key, $val);
            } else {
                update_option($key, $val);
            }
            $saved++;
        } else {
            // Unchecked checkboxes - only clear if it's a known checkbox key
            $checkboxes = array(
                'yatoon_send_confirmation_email', 'yatoon_email_notify_admin',
                'yatoon_reminder_enabled', 'yatoon_review_enabled',
                'yatoon_tip_enabled', 'yatoon_gift_card_enabled', 'yatoon_enable_coupons',
                'yatoon_multi_location_enabled', 'yatoon_auto_sync_enabled',
                'yatoon_booking_sync_enabled', 'yatoon_pwa_enabled', 'yatoon_show_advanced_menu', 'yatoon_test_booking_mode',
                'yatoon_whatsapp_enabled', 'yatoon_text_us_enabled',
                'yatoon_deposit_enabled', 'yatoon_square_payments_enabled', 'yatoon_no_show_followup_enabled',
                'yatoon_google_cal_enabled', 'yatoon_google_cal_send_invites',
                'yatoon_manage_allow_reschedule', 'yatoon_manage_notify_admin_cancel',
                'yatoon_manage_notify_admin_reschedule', 'yatoon_phone_required',
                'yatoon_verify_email_otp', 'yatoon_verify_sms_otp',
                'yatoon_policy_enabled', 'yatoon_policy_require_ack',
                'yatoon_consent_enabled', 'yatoon_waitlist_enabled',
                'yatoon_waitlist_auto_notify', 'yatoon_multi_appointment',
                'yatoon_show_next_opening', 'yatoon_show_availability_source_note',
                'yatoon_show_success_next', 'yatoon_show_venue_hero',
                'yatoon_show_portal_banner', 'yatoon_portal_new_tab',
                'yatoon_show_ratings_on_cards', 'yatoon_booking_immersive_mode',
                'yatoon_twilio_enabled', 'yatoon_twilio_customer_sms',
                'yatoon_twilio_admin_sms', 'yatoon_twilio_reminder_sms', 'yatoon_twilio_logs',
            );
            if (in_array($key, $checkboxes, true)) {
                update_option($key, '0');
            }
        }
    }

    // Reschedule booking sync if interval changed
    if (class_exists('YATOON_Cron')) {
        YATOON_Cron::maybe_reschedule_booking_sync();
    }

    wp_send_json_success(array('message' => 'Settings saved.', 'count' => $saved));
});

// -- Restore default templates ------------------------------------------------
add_action('wp_ajax_yatoon_restore_default_templates', function() {
    check_ajax_referer('yatoon_restore_defaults', 'nonce');
    if (!current_user_can('manage_options')) wp_send_json_error();

    // Delete template/text options so get_option() falls back to built-in defaults
    $reset_keys = array(
        // Email templates
        'yatoon_email_template', 'yatoon_email_subject',
        'yatoon_reminder_template', 'yatoon_reminder_subject',
        'yatoon_review_template', 'yatoon_review_subject',
        'yatoon_cancel_template', 'yatoon_cancel_subject',
        'yatoon_reschedule_template', 'yatoon_reschedule_subject',
        'yatoon_no_show_followup_template', 'yatoon_no_show_followup_subject',
        // Policy / Consent
        'yatoon_policy_text', 'yatoon_policy_title', 'yatoon_policy_ack_label',
        'yatoon_consent_text', 'yatoon_consent_title', 'yatoon_consent_signature_label',
        // SMS templates
        'yatoon_twilio_customer_template', 'yatoon_twilio_admin_template',
        'yatoon_twilio_reminder_template',
        // Success page text fields (have defaults in get_option second arg)
        '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', 'yatoon_availability_help_text',
        // Venue labels
        'yatoon_venue_kicker', 'yatoon_venue_pay_later_label', 'yatoon_venue_deposit_label',
        // Portal labels
        'yatoon_portal_login_title', 'yatoon_portal_login_subtitle',
        'yatoon_portal_btn_label', 'yatoon_portal_banner_text', 'yatoon_portal_banner_prompt',
        // Tip
        'yatoon_tip_label', 'yatoon_tip_presets',
        // WhatsApp
        'yatoon_whatsapp_label', 'yatoon_whatsapp_prefill',
    );
    foreach ($reset_keys as $key) {
        delete_option($key);
    }
    wp_send_json_success(array('message' => 'Default templates restored. Reload the page to see them.'));
});
