<?php
namespace BookWellAB;

defined('ABSPATH') || exit;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

if (!class_exists('BookWellAB_Appointments')) {
    class BookWellAB_Appointments
    {
        private static $instance = null;
        private $bookwell_page = 'appointments';
        private $bwdb;
        private $bw_wp_nonce;

        public static function get_instance()
        {
            if (self::$instance === null) {
                self::$instance = new self();
            }
            return self::$instance;
        }

        /**
         * Payment gateway options for admin appointment form (Manually, Offline, PayPal via filter).
         *
         * @return array<int, array{value: string, label: string}>
         */
        public static function get_admin_payment_gateway_options()
        {
            $settings = \BookWellAB\BookWellAB_Settings::get_settings();
            $payment_settings = isset($settings['payment']) && is_array($settings['payment']) ? $settings['payment'] : [];

            $payment_gateway_options = [];

            $enable_manually = isset($payment_settings['enable_manually']) ? sanitize_text_field($payment_settings['enable_manually']) : 'yes';
            if ($enable_manually !== 'no') {
                $manually_label = isset($payment_settings['manually_label']) && $payment_settings['manually_label'] !== ''
                    ? sanitize_text_field($payment_settings['manually_label'])
                    : esc_html__('Manually', 'bookwellnow-appointment-booking');
                $payment_gateway_options[] = [
                    'value' => 'manually',
                    'label' => $manually_label,
                ];
            }

            $offline_enabled = isset($payment_settings['enable_offline']) ? sanitize_text_field($payment_settings['enable_offline']) : 'yes';
            $offline_label = isset($payment_settings['offline_label']) && $payment_settings['offline_label'] !== ''
                ? sanitize_text_field($payment_settings['offline_label'])
                : esc_html__('Offline', 'bookwellnow-appointment-booking');

            if ($offline_enabled === 'yes') {
                $payment_gateway_options[] = [
                    'value' => 'offline',
                    'label' => $offline_label,
                ];
            } else {
                $payment_gateway_options[] = [
                    'value' => 'offline',
                    'label' => esc_html__('Offline', 'bookwellnow-appointment-booking'),
                ];
            }

            return apply_filters('bookwellab_payment_gateway_options', $payment_gateway_options, $payment_settings);
        }

        public function __construct()
        {
            global $BOOKWELLAB;
            $this->bwdb = new BookWellAB_Database();
            $current_page = (isset($BOOKWELLAB) && $BOOKWELLAB instanceof BOOKWELL)
                ? $BOOKWELLAB->get_bookwell_admin_page()
                : '';

            if (is_admin() && $current_page == $this->bookwell_page) {
                add_filter('bookwellab_admin_script_vriable_data', [$this, 'bookwellab_admin_script_vriable_data'], 10, 2);
                add_action('bookwellab_appointments_admin_html', [$this, 'bookwellab_appointments_admin_html_func'], 10);
                add_filter('bookwellab_page_method_data', [$this, 'bookwellab_page_method_data_func'], 10, 2);
                add_filter('bookwellab_page_mounted_data', [$this, 'bookwellab_page_mounted_data_func'], 10, 2);
                add_action('admin_enqueue_scripts', [$this, 'admin_assets']);
            }

            add_action('wp_ajax_bookwellab_get_appointments', [$this, 'bookwellab_get_appointments_func']);
            add_action('wp_ajax_bookwellab_update_appointment', [$this, 'bookwellab_update_appointment_func']);
            add_action('wp_ajax_bookwellab_create_appointment', [$this, 'bookwellab_create_appointment_func']);
            add_action('wp_ajax_bookwellab_delete_appointment', [$this, 'bookwellab_delete_appointment_func']);
            add_action('wp_ajax_bookwellab_admin_get_month_availability', [$this, 'bookwellab_admin_get_month_availability_func']);
            add_action('wp_ajax_bookwellab_admin_get_time_slots', [$this, 'bookwellab_admin_get_time_slots_func']);
            add_action('wp_ajax_bookwellab_search_customers', [$this, 'bookwellab_search_customers_func']);
            add_action('wp_ajax_bookwellab_quick_add_customer', [$this, 'bookwellab_quick_add_customer_func']);
            add_action('bookwellab_appointment_admin_summary', [$this, 'render_appointment_admin_summary'], 20);
        }

        public function render_appointment_admin_summary()
        {
            require_once BOOKWELLAB_HTML_PATH . 'appointment_admin_summary_view.php';
        }

        public function bookwellab_appointments_admin_html_func()
        {
            require_once BOOKWELLAB_HTML_PATH . 'appointments_view.php';
        }

        public function admin_assets($hook)
        {
            global $BOOKWELLAB;
            if ($BOOKWELLAB->get_bookwell_admin_page($hook) === $this->bookwell_page) {
                \BookWellAB\BookWellAB_Frontend::enqueue_intl_tel_input_assets();
            }
        }

        public function bookwellab_admin_script_vriable_data($bookwellab_admin_script_vriable_data, $bookwellpage)
        {
            if ($this->bookwell_page != $bookwellpage) {
                return $bookwellab_admin_script_vriable_data;
            }

            $filter = [
                'search' => '',
                'status' => '', // all
                'staff_id' => '',
                'service_id' => '',
                'date' => ''
            ];

            // Define reset filter structure
            $bookwellab_admin_script_vriable_data['filter'] = $filter;
            $bookwellab_admin_script_vriable_data['filter_reset'] = $filter;

            $bookwellab_admin_script_vriable_data['status_options'] = [
                ['value' => 'pending', 'label' => __('Pending', 'bookwellnow-appointment-booking')],
                ['value' => 'approved', 'label' => __('Approved', 'bookwellnow-appointment-booking')],
                ['value' => 'cancelled', 'label' => __('Cancelled', 'bookwellnow-appointment-booking')],
                ['value' => 'completed', 'label' => __('Completed', 'bookwellnow-appointment-booking')],
            ];

            $bookwellab_admin_script_vriable_data['status_labels'] = [
                'pending' => __('Pending', 'bookwellnow-appointment-booking'),
                'approved' => __('Approved', 'bookwellnow-appointment-booking'),
                'cancelled' => __('Cancelled', 'bookwellnow-appointment-booking'),
                'completed' => __('Completed', 'bookwellnow-appointment-booking'),
            ];

            $bookwellab_admin_script_vriable_data['payment_status_labels'] = [
                'pending' => __('Pending', 'bookwellnow-appointment-booking'),
                'received' => __('Received', 'bookwellnow-appointment-booking'),
                'failed' => __('Failed', 'bookwellnow-appointment-booking'),
            ];

            $bookwellab_admin_script_vriable_data['payment_status_options'] = [
                ['value' => 'pending', 'label' => __('Pending', 'bookwellnow-appointment-booking')],
                ['value' => 'received', 'label' => __('Received', 'bookwellnow-appointment-booking')],
                ['value' => 'failed', 'label' => __('Failed', 'bookwellnow-appointment-booking')],
            ];
            $bookwellab_admin_script_vriable_data['payment_gateway_options'] = self::get_admin_payment_gateway_options();

            // Fetch Staff list
            global $wpdb;
            $staff_table = esc_sql($this->bwdb->get_table_name('staffs'));
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $all_staff = $wpdb->get_results($wpdb->prepare("SELECT bw_staff_id, bw_staff_name FROM $staff_table WHERE bw_staff_is_active = %d ORDER BY bw_staff_name ASC", 1), ARRAY_A);

            $staff_list = [];
            if (!empty($all_staff)) {
                foreach ($all_staff as $staff) {
                    $staff_list[] = [
                        'value' => $staff['bw_staff_id'],
                        'label' => $staff['bw_staff_name']
                    ];
                }
            }
            $bookwellab_admin_script_vriable_data['staff_list'] = $staff_list;

            // Fetch Service list
            $service_table = esc_sql($this->bwdb->get_table_name('services'));
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $all_services = $wpdb->get_results($wpdb->prepare("SELECT bw_service_id, bw_service_name FROM $service_table WHERE bw_service_status = %d ORDER BY bw_service_name ASC", 1), ARRAY_A);

            $service_list = [];
            if (!empty($all_services)) {
                foreach ($all_services as $service) {
                    $service_list[] = [
                        'value' => $service['bw_service_id'],
                        'label' => $service['bw_service_name']
                    ];
                }
            }
            $bookwellab_admin_script_vriable_data['service_list'] = $service_list;

            // Fetch Staff-Service relationships
            $rel_table = esc_sql($this->bwdb->get_table_name('staff_services'));
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $relationships = $wpdb->get_results("SELECT bw_staff_id, bw_service_id FROM $rel_table", ARRAY_A);
            $bookwellab_admin_script_vriable_data['staff_service_relationships'] = !empty($relationships) ? $relationships : [];

            $bookwellab_admin_script_vriable_data['pagination'] = [
                'page' => 1,
                'limit' => 10,
                'total' => 0
            ];
            $bookwellab_admin_script_vriable_data['pagination_reset'] = [
                'page' => 1,
                'limit' => 10,
                'total' => 0
            ];

            $bookwellab_admin_script_vriable_data['appointment_form'] = [];
            $bookwellab_admin_script_vriable_data['insert_loader'] = '0';
            $bookwellab_admin_script_vriable_data['appointment_customer_options'] = [];
            $bookwellab_admin_script_vriable_data['appointment_customer_search_loading'] = false;
            $bookwellab_admin_script_vriable_data['appointment_customer_search_timeout'] = null;
            $bookwellab_admin_script_vriable_data['appointment_customer_dialog_visible'] = false;
            $bookwellab_admin_script_vriable_data['appointment_customer_save_loader'] = '0';
            $bookwellab_admin_script_vriable_data['appointment_customer_form_reset'] = [
                'bw_customer_id' => 0,
                'bw_first_name' => '',
                'bw_last_name' => '',
                'bw_email' => '',
                'bw_phone_code' => '',
                'bw_phone' => '',
                'bw_country_slug' => '',
            ];
            $bookwellab_admin_script_vriable_data['appointment_customer_form'] = $bookwellab_admin_script_vriable_data['appointment_customer_form_reset'];
            $bookwellab_admin_script_vriable_data['appointment_customer_form_rules'] = [
                'bw_first_name' => [
                    ['required' => true, 'message' => esc_html__('Please enter first name', 'bookwellnow-appointment-booking'), 'trigger' => 'blur'],
                ],
                'bw_email' => [
                    ['required' => true, 'message' => esc_html__('Please enter email', 'bookwellnow-appointment-booking'), 'trigger' => 'blur'],
                    ['type' => 'email', 'message' => esc_html__('Please enter valid email', 'bookwellnow-appointment-booking'), 'trigger' => 'blur'],
                ],
            ];

            $bookwellab_admin_script_vriable_data['appointment_form_reset'] = [
                'bw_appointment_id' => 0,
                'bw_staff_id' => '',
                'bw_service_id' => '',
                'bw_date' => '',
                'bw_time' => '',
                'bw_status' => 'pending',
                'bw_customer_id' => '',
                'bw_customer_name' => '',
                'bw_customer_email' => '',
                'bw_customer_phone_code' => '',
                'bw_customer_phone' => '',
                'bw_customer_country_slug' => '',
                'bw_note' => '',
                'bw_payment_gateway' => 'manually',
                'bw_payment_status' => 'pending',
                'appointment_disabled_dates' => [],
                'appointment_time_slots' => [],
                'appointment_availability_loading' => false,
                'appointment_calendar_month' => '',
                'bw_extras' => [],
                'bw_custom_fields' => [],
            ];

            /**
             * Filters the appointment form reset/default payload for the admin drawer.
             *
             * @param array<string, mixed> $form_reset Default form values.
             */
            $bookwellab_admin_script_vriable_data['appointment_form_reset'] = apply_filters(
                'bookwellab_appointment_form_reset',
                $bookwellab_admin_script_vriable_data['appointment_form_reset']
            );

            $bookwellab_admin_script_vriable_data['search_timeout'] = null;
            $bookwellab_admin_script_vriable_data['list_data'] = array();
            $bookwellab_admin_script_vriable_data['bw_list_loader'] = '0';
            $bookwellab_admin_script_vriable_data['list_table_key'] = 0;
            $bookwellab_admin_script_vriable_data['staff_service_meta'] = $this->get_staff_service_pricing_map();

            return $bookwellab_admin_script_vriable_data;
        }

        public function bookwellab_page_mounted_data_func($bookwellab_page_mounted_data, $bookwellpage)
        {
            if ($this->bookwell_page != $bookwellpage) {
                return $bookwellab_page_mounted_data;
            }

            $bookwellab_page_mounted_data .= '
                const vuedata = this;
                vuedata.bwab_getAppointments();
            ';

            return $bookwellab_page_mounted_data;
        }

        public function bookwellab_page_method_data_func($bookwellab_page_method_data, $bookwellpage)
        {
            if ($this->bookwell_page != $bookwellpage) {
                return $bookwellab_page_method_data;
            }

            $this->bw_wp_nonce = esc_html(wp_create_nonce('bw_wp_nonce'));

            $bookwellab_page_method_data .= '
                bwab_getPickerFormat() {
                    var format = bookwellab_admin_script_vriable_data.date_format || "Y-m-d";
                    var map = {
                        "Y": "YYYY",
                        "m": "MM",
                        "d": "DD",
                        "j": "D",
                        "F": "MMMM",
                        "M": "MMM",
                        "y": "YY"
                    };
                    return format.replace(/[YmdjFMy]/g, function(matched){
                        return map[matched];
                    });
                },

                bwab_getNotificationOffset() {
                    const adminBar = document.getElementById("wpadminbar");
                    if (!adminBar) {
                        return 16;
                    }
                    return adminBar.offsetHeight + 12;
                },

                async bwab_getAppointments(show_loader = true) {
                    var ajax_url = bookwellab_ajax_obj.ajax_url;
                    const vuedata = this;
                    
                    if(show_loader) {
                        vuedata.bw_list_loader = "1";
                    }
                    
                    var postData = {
                        action: "bookwellab_get_appointments",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        filter: vuedata.filter,
                        page: vuedata.pagination.page,
                        limit: vuedata.pagination.limit
                    };
                    
                    return axios.post(ajax_url, Qs.stringify(postData))
                    .then(function(response) {
                        vuedata.bw_list_loader = "0";
                        var rawData = response.data;

                        // Check for WordPress error format
                        if (rawData.success === false) {
                            vuedata.$notify({
                                title: "Error",
                                message: rawData.data.msg || "Failed to load appointments",
                                type: "error",
                                duration: 3000,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });
                            return;
                        }
                        
                        // Check for custom error format
                        if (rawData.type === "error") {
                             vuedata.$notify({
                                title: "Error",
                                message: rawData.msg || "Failed to load appointments",
                                type: "error",
                                duration: 3000,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });
                            return;
                        }
                        
                        var list = [];
                        
                        if (rawData.list_data) {
                            list = rawData.list_data;
                        } else if (Array.isArray(rawData)) {
                            list = rawData;
                        } else if (rawData && rawData.bw_appointment_id) {
                            list = [rawData];
                        }
                        
                        vuedata.list_data = Array.isArray(list) ? list.slice() : [];
                        vuedata.list_table_key = (parseInt(vuedata.list_table_key, 10) || 0) + 1;

                        if (rawData.total) {
                            vuedata.pagination.total = parseInt(rawData.total, 10);
                        } else {
                            vuedata.pagination.total = 0;
                        }

                        return vuedata.list_data;

                    }).catch(function(error) {
                        console.log(error);
                        vuedata.bw_list_loader = "0";
                        vuedata.bwab_error_handler();
                    });
                },

                bwab_getFilteredServices() {
                    const vuedata = this;
                    const staffId = vuedata.filter.staff_id;
                    const relationships = bookwellab_admin_script_vriable_data.staff_service_relationships || [];
                    const services = bookwellab_admin_script_vriable_data.service_list || [];

                    if (!staffId) {
                        return services;
                    }

                    // Get assigned service IDs for the selected staff
                    const assignedServiceIds = relationships
                        .filter(rel => parseInt(rel.bw_staff_id) === parseInt(staffId))
                        .map(rel => parseInt(rel.bw_service_id));

                    // Filter the master service list
                    return services.filter(service => assignedServiceIds.includes(parseInt(service.value)));
                },

                bwab_getFormFilteredServices() {
                    const vuedata = this;
                    const staffId = vuedata.appointment_form ? vuedata.appointment_form.bw_staff_id : "";
                    const relationships = bookwellab_admin_script_vriable_data.staff_service_relationships || [];
                    const services = bookwellab_admin_script_vriable_data.service_list || [];

                    if (!staffId) {
                        return services;
                    }

                    const assignedServiceIds = relationships
                        .filter(rel => parseInt(rel.bw_staff_id) === parseInt(staffId))
                        .map(rel => parseInt(rel.bw_service_id));

                    return services.filter(service => assignedServiceIds.includes(parseInt(service.value)));
                },

                bwab_normalizeAppointmentTime(timeStr) {
                    if (!timeStr) {
                        return "";
                    }
                    const parts = String(timeStr).split(":");
                    if (parts.length >= 2) {
                        return String(parseInt(parts[0], 10)).padStart(2, "0") + ":" + String(parseInt(parts[1], 10)).padStart(2, "0");
                    }
                    return timeStr;
                },

                bwab_addAppointment() {
                    const vuedata = this;
                    vuedata.appointment_form = JSON.parse(JSON.stringify(vuedata.appointment_form_reset));
                    vuedata.appointment_customer_options = [];
                    if (typeof vuedata.bwab_resetAppointmentExtrasState === "function") {
                        vuedata.bwab_resetAppointmentExtrasState();
                    }
                    vuedata.bw_add_edit_popup = true;
                },

                bwab_onAppointmentStaffChange() {
                    const vuedata = this;
                    const form = vuedata.appointment_form;

                    if (form.bw_staff_id && form.bw_service_id) {
                        const filteredServices = vuedata.bwab_getFormFilteredServices();
                        const isServiceValid = filteredServices.some(function(service) {
                            return parseInt(service.value, 10) === parseInt(form.bw_service_id, 10);
                        });
                        if (!isServiceValid) {
                            form.bw_service_id = "";
                        }
                    } else {
                        form.bw_service_id = "";
                    }

                    form.bw_date = "";
                    form.bw_time = "";
                    form.appointment_time_slots = [];
                    form.appointment_disabled_dates = [];

                    if (form.bw_staff_id && form.bw_service_id) {
                        const now = new Date();
                        vuedata.bwab_loadAppointmentMonthAvailability(now.getFullYear(), now.getMonth() + 1);
                    }

                    if (typeof vuedata.bwab_onAppointmentStaffExtrasChange === "function") {
                        vuedata.bwab_onAppointmentStaffExtrasChange();
                    }
                },

                bwab_onAppointmentServiceChange() {
                    const vuedata = this;
                    const form = vuedata.appointment_form;

                    form.bw_date = "";
                    form.bw_time = "";
                    form.appointment_time_slots = [];
                    form.appointment_disabled_dates = [];

                    if (form.bw_staff_id && form.bw_service_id) {
                        const now = new Date();
                        vuedata.bwab_loadAppointmentMonthAvailability(now.getFullYear(), now.getMonth() + 1);
                    }

                    if (typeof vuedata.bwab_onAppointmentServiceExtrasChange === "function") {
                        vuedata.bwab_onAppointmentServiceExtrasChange(true);
                    }
                },

                async bwab_loadAppointmentMonthAvailability(year, month) {
                    const vuedata = this;
                    const form = vuedata.appointment_form;
                    const ajax_url = bookwellab_ajax_obj.ajax_url;

                    if (!form.bw_staff_id || !form.bw_service_id || !year || !month) {
                        form.appointment_disabled_dates = [];
                        return;
                    }

                    form.appointment_calendar_month = year + "-" + String(month).padStart(2, "0");

                    const postData = {
                        action: "bookwellab_admin_get_month_availability",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        year: year,
                        month: month,
                        staff_id: form.bw_staff_id,
                        service_id: form.bw_service_id,
                        exclude_appointment_id: form.bw_appointment_id || 0,
                        extras: JSON.stringify(form.bw_extras || [])
                    };

                    try {
                        form.appointment_availability_loading = true;
                        const response = await axios.post(ajax_url, Qs.stringify(postData));
                        const rawData = response.data || {};

                        if (rawData.type === "success" && Array.isArray(rawData.disabled_dates)) {
                            form.appointment_disabled_dates = rawData.disabled_dates;
                        } else {
                            form.appointment_disabled_dates = [];
                            if (rawData.type === "error" && rawData.msg) {
                                vuedata.$notify({
                                    title: "Error",
                                    message: rawData.msg,
                                    type: "error",
                                    duration: 3000,
                                    offset: vuedata.bwab_getNotificationOffset(),
                                });
                            }
                        }
                    } catch (error) {
                        console.log(error);
                        form.appointment_disabled_dates = [];
                    } finally {
                        form.appointment_availability_loading = false;
                    }
                },

                bwab_onAppointmentCalendarPanelChange(dateObj) {
                    const vuedata = this;
                    if (!dateObj) {
                        return;
                    }

                    const year = dateObj.getFullYear();
                    const month = dateObj.getMonth() + 1;
                    const monthKey = year + "-" + String(month).padStart(2, "0");

                    if (vuedata.appointment_form.appointment_calendar_month === monthKey) {
                        return;
                    }

                    vuedata.bwab_loadAppointmentMonthAvailability(year, month);
                },

                bwab_isAppointmentDateDisabled(dateObj) {
                    const vuedata = this;
                    if (!dateObj) {
                        return true;
                    }

                    const y = dateObj.getFullYear();
                    const m = String(dateObj.getMonth() + 1).padStart(2, "0");
                    const d = String(dateObj.getDate()).padStart(2, "0");
                    const dateStr = y + "-" + m + "-" + d;
                    const disabledDates = vuedata.appointment_form.appointment_disabled_dates || [];

                    return disabledDates.includes(dateStr);
                },

                async bwab_onAppointmentDateChange(date, clearTime = true) {
                    const vuedata = this;
                    const form = vuedata.appointment_form;
                    const ajax_url = bookwellab_ajax_obj.ajax_url;
                    const preservedTime = clearTime
                        ? ""
                        : vuedata.bwab_normalizeAppointmentTime(form.bw_time);

                    if (clearTime) {
                        form.bw_time = "";
                        form.appointment_time_slots = [];
                    } else if (preservedTime) {
                        // Keep current time selectable while slots reload (el-select clears model if options empty).
                        form.bw_time = preservedTime;
                        if (!Array.isArray(form.appointment_time_slots) || !form.appointment_time_slots.includes(preservedTime)) {
                            form.appointment_time_slots = [preservedTime];
                        }
                    }

                    if (!date || !form.bw_staff_id || !form.bw_service_id) {
                        return;
                    }

                    const postData = {
                        action: "bookwellab_admin_get_time_slots",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        date: date,
                        staff_id: form.bw_staff_id,
                        service_id: form.bw_service_id,
                        exclude_appointment_id: form.bw_appointment_id || 0,
                        extras: JSON.stringify(form.bw_extras || [])
                    };

                    try {
                        form.appointment_availability_loading = true;
                        const response = await axios.post(ajax_url, Qs.stringify(postData));
                        const rawData = response.data || {};

                        if (rawData.type === "success" && Array.isArray(rawData.slots)) {
                            let slots = rawData.slots.slice();

                            if (!clearTime && preservedTime) {
                                if (!slots.includes(preservedTime)) {
                                    slots.unshift(preservedTime);
                                }
                                form.appointment_time_slots = slots;
                                form.bw_time = preservedTime;
                            } else {
                                form.appointment_time_slots = slots;
                                if (form.bw_time) {
                                    const normalizedTime = vuedata.bwab_normalizeAppointmentTime(form.bw_time);
                                    form.bw_time = slots.includes(normalizedTime) ? normalizedTime : "";
                                }
                            }
                        } else if (rawData.type === "error" && rawData.msg) {
                            vuedata.$notify({
                                title: "Error",
                                message: rawData.msg,
                                type: "error",
                                duration: 3000,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });
                        }
                    } catch (error) {
                        console.log(error);
                    } finally {
                        form.appointment_availability_loading = false;
                    }
                },

                bwab_handleStaffChange() {
                    const vuedata = this;
                    // If staff selection changes, verify if the currently selected service is still valid
                    if(vuedata.filter.staff_id && vuedata.filter.service_id) {
                        const filteredServices = vuedata.bwab_getFilteredServices();
                        const isServiceValid = filteredServices.some(s => parseInt(s.value) === parseInt(vuedata.filter.service_id));
                        
                        if(!isServiceValid) {
                            vuedata.filter.service_id = ""; // Clear service filter if invalid for selected staff
                        }
                    }
                    vuedata.bwab_getAppointments();
                },

                bwab_searchAppointments() {
                    const vuedata = this;
                    clearTimeout(vuedata.search_timeout);
                    vuedata.search_timeout = setTimeout(() => {
                        vuedata.bwab_getAppointments();
                    }, 600); // Wait 600ms after last keystroke
                },

                bwab_handleSizeChange(val) {
                    const vuedata = this;
                    vuedata.pagination.limit = val;
                    vuedata.pagination.page = 1;
                    vuedata.bwab_getAppointments();
                },
                
                bwab_handleCurrentChange(val) {
                    const vuedata = this;
                    vuedata.pagination.page = val;
                    vuedata.bwab_getAppointments();
                },

                bwab_editRow(row) {
                    const vuedata = this;
                    vuedata.appointment_form = Object.assign(
                        JSON.parse(JSON.stringify(vuedata.appointment_form_reset)),
                        JSON.parse(JSON.stringify(row))
                    );
                    if (!vuedata.appointment_form.bw_payment_gateway) {
                        vuedata.appointment_form.bw_payment_gateway = "manually";
                    }
                    const normalizedEditTime = vuedata.bwab_normalizeAppointmentTime(vuedata.appointment_form.bw_time);
                    vuedata.appointment_form.bw_time = normalizedEditTime;
                    // Seed slots with the current time so el-select does not clear the value while options reload.
                    vuedata.appointment_form.appointment_time_slots = normalizedEditTime ? [normalizedEditTime] : [];
                    vuedata.appointment_form.appointment_disabled_dates = [];
                    vuedata.appointment_form.appointment_availability_loading = false;
                    vuedata.bwab_seedAppointmentCustomerOption(vuedata.appointment_form);
                    if (!Array.isArray(vuedata.appointment_form.bw_extras)) {
                        vuedata.appointment_form.bw_extras = [];
                    }
                    if (!vuedata.appointment_form.bw_custom_fields || typeof vuedata.appointment_form.bw_custom_fields !== "object" || Array.isArray(vuedata.appointment_form.bw_custom_fields)) {
                        vuedata.appointment_form.bw_custom_fields = (vuedata.appointment_form_reset && vuedata.appointment_form_reset.bw_custom_fields)
                            ? JSON.parse(JSON.stringify(vuedata.appointment_form_reset.bw_custom_fields))
                            : {};
                    }
                    // Do not call service-extras "change" here — that resets extras/time for service changes only.
                    vuedata.bw_add_edit_popup = true;

                    vuedata.$nextTick(function() {
                        if (!vuedata.appointment_form.bw_staff_id || !vuedata.appointment_form.bw_service_id) {
                            return;
                        }

                        const dateParts = String(vuedata.appointment_form.bw_date || "").split("-");
                        const year = dateParts.length >= 1 ? parseInt(dateParts[0], 10) : new Date().getFullYear();
                        const month = dateParts.length >= 2 ? parseInt(dateParts[1], 10) : (new Date().getMonth() + 1);

                        vuedata.bwab_loadAppointmentMonthAvailability(year, month).then(function() {
                            if (vuedata.appointment_form.bw_date) {
                                return vuedata.bwab_onAppointmentDateChange(vuedata.appointment_form.bw_date, false);
                            }
                        });
                    });
                },

                bwab_init_iti() {
                    const input = document.querySelector("#bw_appointment_phone");
                    if (!input || typeof window.intlTelInput !== "function") {
                        return;
                    }

                    if (window.iti_appt_admin) {
                        window.iti_appt_admin.destroy();
                        window.iti_appt_admin = null;
                    }

                    const settingsGeneral = (typeof bookwellab_admin_script_vriable_data !== "undefined" && bookwellab_admin_script_vriable_data.settings_data && bookwellab_admin_script_vriable_data.settings_data.general)
                        ? bookwellab_admin_script_vriable_data.settings_data.general
                        : {};
                    const default_country = settingsGeneral.default_country || "us";
                    const itiOptions = {
                        separateDialCode: true,
                        initialCountry: default_country
                    };

                    window.iti_appt_admin = window.intlTelInput(input, itiOptions);

                    if (this.appointment_form) {
                        const savedPhone = String(this.appointment_form.bw_customer_phone || "").trim();
                        const savedCountrySlug = String(this.appointment_form.bw_customer_country_slug || "").toLowerCase();

                        if (savedCountrySlug) {
                            window.iti_appt_admin.setCountry(savedCountrySlug);
                        } else if (this.appointment_form.bw_customer_phone_code) {
                            const savedDialCode = String(this.appointment_form.bw_customer_phone_code).replace("+", "");
                            const countryDataList = (typeof window.intlTelInput.getCountryData === "function")
                                ? window.intlTelInput.getCountryData()
                                : [];
                            const matchedCountry = countryDataList.find((country) => String(country.dialCode) === savedDialCode);

                            if (matchedCountry && matchedCountry.iso2) {
                                window.iti_appt_admin.setCountry(matchedCountry.iso2);
                            }
                        }

                        if (savedPhone) {
                            input.value = savedPhone;
                            this.appointment_form.bw_customer_phone = savedPhone;
                        }
                    }
                },

                async bwab_deleteRow(row, index) {
                    const vuedata = this;
                    var ajax_url = bookwellab_ajax_obj.ajax_url;
                    
                    row.delete_loader = "1";
                    
                    var postData = {
                        action: "bookwellab_delete_appointment",
                        id: row.bw_appointment_id,
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce
                    };
                    
                    try {
                        const response = await axios.post(ajax_url, Qs.stringify(postData));
                        row.delete_loader = "0";
                        var rawData = response.data || {};
                        var isError = (rawData.success === false) || (rawData.type === "error");

                        if (isError) {
                            vuedata.$notify({
                                title: "Error",
                                message: (rawData.data && rawData.data.msg) || rawData.msg || "Failed to delete appointment",
                                type: "error",
                                customClass: "error_notification",
                                duration: 2200,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });
                            return;
                        }

                        if (vuedata.pagination.total > 0) {
                            vuedata.pagination.total = vuedata.pagination.total - 1;
                        }

                        const isLastRowOnPage = vuedata.list_data.length === 1;
                        if (isLastRowOnPage && vuedata.pagination.page > 1) {
                            vuedata.pagination.page = vuedata.pagination.page - 1;
                        }

                        // Remove the row immediately for responsive UI, then sync with DB.
                        vuedata.list_data = vuedata.list_data.filter(function(item){
                            return parseInt(item.bw_appointment_id) !== parseInt(row.bw_appointment_id);
                        });

                        await vuedata.bwab_getAppointments(true);

                        vuedata.$notify({
                            title: rawData.title || "Success",
                            message: rawData.msg || "Appointment deleted successfully.",
                            type: rawData.type || "success",
                            customClass: "success_notification",
                            duration: 2200,
                            offset: vuedata.bwab_getNotificationOffset(),
                        });
                    } catch (error) {
                        row.delete_loader = "0";
                        console.log(error);
                        vuedata.bwab_error_handler();
                    }
                },

                bwab_patchListRow(appointmentRow) {
                    const vuedata = this;
                    if (!appointmentRow || !appointmentRow.bw_appointment_id || !Array.isArray(vuedata.list_data)) {
                        return;
                    }

                    const appointmentId = parseInt(appointmentRow.bw_appointment_id, 10);
                    const rowIndex = vuedata.list_data.findIndex(function(item) {
                        return parseInt(item.bw_appointment_id, 10) === appointmentId;
                    });

                    if (rowIndex === -1) {
                        return;
                    }

                    vuedata.list_data.splice(rowIndex, 1, Object.assign({}, vuedata.list_data[rowIndex], appointmentRow));
                    vuedata.list_table_key = (parseInt(vuedata.list_table_key, 10) || 0) + 1;
                },

                bwab_syncAppointmentPhoneFields() {
                    const vuedata = this;
                    if (!window.iti_appt_admin || !vuedata.appointment_form) {
                        return;
                    }

                    const countryData = window.iti_appt_admin.getSelectedCountryData();
                    if (countryData && countryData.dialCode) {
                        vuedata.appointment_form.bw_customer_phone_code = "+" + countryData.dialCode;
                    }
                    if (countryData && countryData.iso2) {
                        vuedata.appointment_form.bw_customer_country_slug = String(countryData.iso2).toLowerCase();
                    }
                    vuedata.appointment_form.bw_customer_phone = String(vuedata.appointment_form.bw_customer_phone || "").replace(/\D/g, "");
                },

                bwab_getCustomerDisplayName(customer) {
                    if (!customer) {
                        return "";
                    }
                    const firstName = String(customer.bw_first_name || customer.bw_customer_name || "").trim();
                    const lastName = String(customer.bw_last_name || "").trim();
                    return (firstName + " " + lastName).trim();
                },

                bwab_getCustomerOptionLabel(customer) {
                    if (!customer) {
                        return "";
                    }
                    const name = this.bwab_getCustomerDisplayName(customer);
                    const email = String(customer.bw_email || "").trim();
                    if (name && email) {
                        return name + " (" + email + ")";
                    }
                    return name || email || "";
                },

                bwab_searchAppointmentCustomers(query) {
                    const vuedata = this;
                    const searchQuery = String(query || "").trim();

                    clearTimeout(vuedata.appointment_customer_search_timeout);

                    if (searchQuery.length < 1) {
                        vuedata.appointment_customer_options = [];
                        return;
                    }

                    vuedata.appointment_customer_search_timeout = setTimeout(function() {
                        vuedata.appointment_customer_search_loading = true;

                        axios.post(bookwellab_ajax_obj.ajax_url, Qs.stringify({
                            action: "bookwellab_search_customers",
                            _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                            search: searchQuery,
                        }))
                        .then(function(response) {
                            const rawData = response.data || {};
                            if (rawData.type === "success" && Array.isArray(rawData.customers)) {
                                vuedata.appointment_customer_options = rawData.customers;
                            } else {
                                vuedata.appointment_customer_options = [];
                            }
                        })
                        .catch(function(error) {
                            console.log(error);
                            vuedata.appointment_customer_options = [];
                        })
                        .finally(function() {
                            vuedata.appointment_customer_search_loading = false;
                        });
                    }, 300);
                },

                bwab_applyAppointmentCustomerToForm(customer) {
                    const vuedata = this;
                    if (!customer || !vuedata.appointment_form) {
                        return;
                    }

                    vuedata.appointment_form.bw_customer_id = customer.bw_customer_id || "";
                    vuedata.appointment_form.bw_customer_name = vuedata.bwab_getCustomerDisplayName(customer);
                    vuedata.appointment_form.bw_customer_email = customer.bw_email || "";
                    vuedata.appointment_form.bw_customer_phone_code = customer.bw_phone_code || "";
                    vuedata.appointment_form.bw_customer_phone = customer.bw_phone || "";
                    vuedata.appointment_form.bw_customer_country_slug = customer.bw_country_slug || "";
                },

                bwab_onAppointmentCustomerSelect(customerId) {
                    const vuedata = this;
                    if (!customerId) {
                        return;
                    }

                    const customer = (vuedata.appointment_customer_options || []).find(function(item) {
                        return parseInt(item.bw_customer_id, 10) === parseInt(customerId, 10);
                    });

                    if (customer) {
                        vuedata.bwab_applyAppointmentCustomerToForm(customer);
                        vuedata.$nextTick(function() {
                            if (vuedata.$refs.bw_appt_frm && typeof vuedata.$refs.bw_appt_frm.validateField === "function") {
                                vuedata.$refs.bw_appt_frm.validateField("bw_customer_id");
                            }
                        });
                    }
                },

                bwab_onAppointmentCustomerClear() {
                    const vuedata = this;
                    if (!vuedata.appointment_form) {
                        return;
                    }

                    vuedata.appointment_form.bw_customer_id = "";
                    vuedata.appointment_form.bw_customer_name = "";
                    vuedata.appointment_form.bw_customer_email = "";
                    vuedata.appointment_form.bw_customer_phone_code = "";
                    vuedata.appointment_form.bw_customer_phone = "";
                    vuedata.appointment_form.bw_customer_country_slug = "";
                    vuedata.appointment_customer_options = [];
                },

                bwab_seedAppointmentCustomerOption(row) {
                    const vuedata = this;
                    if (!row || !row.bw_customer_id) {
                        vuedata.appointment_customer_options = [];
                        return;
                    }

                    vuedata.appointment_customer_options = [{
                        bw_customer_id: row.bw_customer_id,
                        bw_first_name: row.bw_customer_name || "",
                        bw_last_name: "",
                        bw_email: row.bw_customer_email || "",
                        bw_phone: row.bw_customer_phone || "",
                        bw_phone_code: row.bw_customer_phone_code || "",
                        bw_country_slug: row.bw_customer_country_slug || "",
                    }];
                },

                bwab_openAppointmentCustomerDialog() {
                    const vuedata = this;
                    vuedata.appointment_customer_form = JSON.parse(JSON.stringify(vuedata.appointment_customer_form_reset));
                    vuedata.appointment_customer_dialog_visible = true;
                    vuedata.$nextTick(function() {
                        vuedata.bwab_initApptCustomerIti();
                    });
                },

                bwab_initApptCustomerIti() {
                    const input = document.querySelector("#bw_appt_customer_phone");
                    if (!input || typeof window.intlTelInput !== "function") {
                        return;
                    }

                    if (window.iti_appt_customer_admin) {
                        window.iti_appt_customer_admin.destroy();
                        window.iti_appt_customer_admin = null;
                    }

                    const settingsGeneral = (typeof bookwellab_admin_script_vriable_data !== "undefined" && bookwellab_admin_script_vriable_data.settings_data && bookwellab_admin_script_vriable_data.settings_data.general)
                        ? bookwellab_admin_script_vriable_data.settings_data.general
                        : {};
                    const default_country = settingsGeneral.default_country || "us";

                    window.iti_appt_customer_admin = window.intlTelInput(input, {
                        separateDialCode: true,
                        initialCountry: default_country,
                    });
                },

                bwab_saveAppointmentCustomer() {
                    const vuedata = this;
                    const ajax_url = bookwellab_ajax_obj.ajax_url;

                    if (!vuedata.$refs.bw_appt_customer_frm || typeof vuedata.$refs.bw_appt_customer_frm.validate !== "function") {
                        return;
                    }

                    vuedata.$refs.bw_appt_customer_frm.validate(function(valid) {
                        if (!valid) {
                            return;
                        }

                        vuedata.appointment_customer_save_loader = "1";

                        if (window.iti_appt_customer_admin) {
                            const countryData = window.iti_appt_customer_admin.getSelectedCountryData();
                            if (countryData && countryData.dialCode) {
                                vuedata.appointment_customer_form.bw_phone_code = "+" + countryData.dialCode;
                            }
                            if (countryData && countryData.iso2) {
                                vuedata.appointment_customer_form.bw_country_slug = String(countryData.iso2).toLowerCase();
                            }
                            vuedata.appointment_customer_form.bw_phone = String(vuedata.appointment_customer_form.bw_phone || "").replace(/\D/g, "");
                        }

                        axios.post(ajax_url, Qs.stringify({
                            action: "bookwellab_quick_add_customer",
                            customer_data: JSON.stringify(vuedata.appointment_customer_form),
                            _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        }))
                        .then(function(response) {
                            const rawData = response.data || {};
                            const isSuccess = rawData.type === "success";

                            vuedata.$notify({
                                title: rawData.title || (isSuccess ? "Success" : "Error"),
                                message: rawData.msg || (isSuccess ? "Customer added successfully." : "Failed to add customer."),
                                type: rawData.type || (isSuccess ? "success" : "error"),
                                duration: 2200,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });

                            if (!isSuccess || !rawData.customer) {
                                vuedata.appointment_customer_save_loader = "0";
                                return;
                            }

                            vuedata.appointment_customer_options = [rawData.customer];
                            vuedata.bwab_applyAppointmentCustomerToForm(rawData.customer);
                            vuedata.appointment_customer_dialog_visible = false;
                            vuedata.appointment_customer_save_loader = "0";
                            vuedata.$nextTick(function() {
                                if (vuedata.$refs.bw_appt_frm && typeof vuedata.$refs.bw_appt_frm.validateField === "function") {
                                    vuedata.$refs.bw_appt_frm.validateField("bw_customer_id");
                                }
                            });
                        })
                        .catch(function(error) {
                            console.log(error);
                            vuedata.appointment_customer_save_loader = "0";
                            vuedata.bwab_error_handler();
                        });
                    });
                },

                bwab_closeDrawer() {
                    const vuedata = this;
                    if (window.iti_appt_admin) {
                        window.iti_appt_admin.destroy();
                        window.iti_appt_admin = null;
                    }
                    if (window.iti_appt_customer_admin) {
                        window.iti_appt_customer_admin.destroy();
                        window.iti_appt_customer_admin = null;
                    }
                    if (vuedata.$refs.bw_appt_frm && typeof vuedata.$refs.bw_appt_frm.resetFields === "function") {
                        vuedata.$refs.bw_appt_frm.resetFields();
                    }
                    vuedata.appointment_form = JSON.parse(JSON.stringify(vuedata.appointment_form_reset));
                    vuedata.appointment_customer_options = [];
                    vuedata.appointment_customer_dialog_visible = false;
                    vuedata.appointment_customer_form = JSON.parse(JSON.stringify(vuedata.appointment_customer_form_reset));
                    if (typeof vuedata.bwab_resetAppointmentExtrasState === "function") {
                        vuedata.bwab_resetAppointmentExtrasState();
                    }
                    vuedata.bw_add_edit_popup = false;
                    vuedata.insert_loader = "0";
                },

                bwab_getStaffServiceMeta(staffId, serviceId) {
                    const data = bookwellab_admin_script_vriable_data || {};
                    const map = data.staff_service_meta || data.pro_staff_service_meta || {};
                    const key = String(staffId || "") + "_" + String(serviceId || "");
                    return map[key] || { price: 0, duration: 0 };
                },

                bwab_getAppointmentServiceSubtotal() {
                    const staffId = parseInt(this.appointment_form.bw_staff_id, 10) || 0;
                    const serviceId = parseInt(this.appointment_form.bw_service_id, 10) || 0;
                    if (!staffId || !serviceId) {
                        return 0;
                    }
                    const meta = this.bwab_getStaffServiceMeta(staffId, serviceId);
                    return parseFloat(meta.price) || 0;
                },

                bwab_shouldShowLiteAppointmentSummary() {
                    const staffId = parseInt(this.appointment_form.bw_staff_id, 10) || 0;
                    const serviceId = parseInt(this.appointment_form.bw_service_id, 10) || 0;
                    if (!staffId || !serviceId) {
                        return false;
                    }
                    if (typeof this.bwab_proGetAppointmentExtrasSummary === "function"
                        && typeof this.bwab_proHasExtrasForService === "function"
                        && this.bwab_proHasExtrasForService(serviceId)
                        && this.bwab_proGetAppointmentExtrasSummary().length > 0) {
                        return false;
                    }
                    return true;
                },

                bwab_formatAppointmentPrice(price) {
                    if (typeof this.bwab_formatApptExtraPrice === "function") {
                        return this.bwab_formatApptExtraPrice(price);
                    }
                    const settings = this.settings_data && this.settings_data.general ? this.settings_data.general : {};
                    const symbol = settings.currency_symbol || "$";
                    const position = settings.currency_position || "before";
                    const amount = parseFloat(price);
                    const value = isNaN(amount) ? "0" : String(amount);
                    return position === "after" ? value + symbol : symbol + value;
                },

                bwab_saveAppointment() {
                    var ajax_url = bookwellab_ajax_obj.ajax_url;
                    const vuedata = this;

                    if (!vuedata.$refs.bw_appt_frm || typeof vuedata.$refs.bw_appt_frm.validate !== "function") {
                        vuedata.$notify({
                            title: "Error",
                            message: "Appointment form is not ready. Please close and reopen the drawer.",
                            type: "error",
                            duration: 3000,
                            offset: vuedata.bwab_getNotificationOffset(),
                        });
                        return;
                    }

                    if (!vuedata.appointment_form.bw_custom_fields || typeof vuedata.appointment_form.bw_custom_fields !== "object" || Array.isArray(vuedata.appointment_form.bw_custom_fields)) {
                        vuedata.appointment_form.bw_custom_fields = {};
                    }

                    vuedata.$refs.bw_appt_frm.validate((valid, fields) => {
                        if (!valid) {
                            const firstKey = fields && typeof fields === "object" ? Object.keys(fields)[0] : "";
                            const firstMsg = firstKey && fields[firstKey] && fields[firstKey][0] && fields[firstKey][0].message
                                ? fields[firstKey][0].message
                                : "Please fill in all required fields.";
                            vuedata.$notify({
                                title: "Error",
                                message: firstMsg,
                                type: "error",
                                duration: 3000,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });
                            vuedata.$nextTick(function() {
                                const errorEl = document.querySelector(".bw-service-drawer .el-form-item.is-error");
                                if (errorEl && typeof errorEl.scrollIntoView === "function") {
                                    errorEl.scrollIntoView({ behavior: "smooth", block: "center" });
                                }
                            });
                            return;
                        }

                        if (!vuedata.appointment_form.bw_time) {
                            vuedata.$notify({
                                title: "Error",
                                message: "Please select a time slot.",
                                type: "error",
                                duration: 3000,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });
                            return;
                        }

                        if (typeof vuedata.bwab_validateAppointmentExtras === "function") {
                            const extrasValidation = vuedata.bwab_validateAppointmentExtras();
                            if (extrasValidation !== true) {
                                vuedata.$notify({
                                    title: "Error",
                                    message: extrasValidation || "Please review your extras selection.",
                                    type: "error",
                                    duration: 3000,
                                    offset: vuedata.bwab_getNotificationOffset(),
                                });
                                return;
                            }
                        }

                        vuedata.insert_loader = "1";
                        vuedata.bwab_syncAppointmentPhoneFields();

                        const isEdit = parseInt(vuedata.appointment_form.bw_appointment_id, 10) > 0;
                        var pass_data = {
                            appt_data: JSON.stringify(vuedata.appointment_form),
                            action: isEdit ? "bookwellab_update_appointment" : "bookwellab_create_appointment",
                            _wpnonce: bookwellab_ajax_obj.bookwellab_nonce
                        };

                        axios.post(ajax_url, Qs.stringify(pass_data))
                        .then(function(response) {
                            const rawData = response.data || {};
                            const isSuccess = rawData.type === "success" || rawData.success === true;
                            const errorMessage = rawData.msg
                                || (rawData.data && rawData.data.msg ? rawData.data.msg : "")
                                || (isSuccess ? (isEdit ? "Updated successfully" : "Created successfully") : "Save failed");

                            vuedata.$notify({
                                title: rawData.title || (isSuccess ? "Success" : "Error"),
                                message: errorMessage,
                                type: rawData.type || (isSuccess ? "success" : "error"),
                                customClass: (rawData.type || (isSuccess ? "success" : "error")) + "_notification",
                                duration: 2200,
                                offset: vuedata.bwab_getNotificationOffset(),
                            });

                            if (!isSuccess) {
                                vuedata.insert_loader = "0";
                                return;
                            }

                            if (rawData.appointment && isEdit) {
                                vuedata.bwab_patchListRow(rawData.appointment);
                            }

                            return vuedata.bwab_getAppointments(false).then(function() {
                                vuedata.insert_loader = "0";
                                vuedata.bwab_closeDrawer();
                            });
                        })
                        .catch(function(error) {
                            console.log(error);
                            vuedata.insert_loader = "0";
                            vuedata.bwab_error_handler();
                        });
                    });
                },

                bwab_formatDate(dateStr) {
                    if(!dateStr) return "";
                    var format = bookwellab_admin_script_vriable_data.date_format || "Y-m-d";
                    var d = new Date(dateStr);
                    if(isNaN(d)) return dateStr;
                    
                    var options = {};
                    if(format.includes("F") || format.includes("M")) options.month = "long";
                    else if(format.includes("m")) options.month = "2-digit";
                    else options.month = "numeric";
                    
                    if(format.includes("d")) options.day = "2-digit";
                    else options.day = "numeric";
                    
                    if(format.includes("Y")) options.year = "numeric";
                    else options.year = "2-digit";
                    
                    // Allow simple override for testing: if format is just Y-m-d, use iso slice
                    if(format == "Y-m-d") return dateStr; 

                    const locale = bookwellab_admin_script_vriable_data.calendar_locale || undefined;
                    return d.toLocaleDateString(locale, options);
                },

                bwab_formatStatus(status) {
                    const labels = bookwellab_admin_script_vriable_data.status_labels || {};
                    if (!status) return "";
                    return labels[status] || status;
                },

                bwab_formatPaymentStatus(status) {
                    const labels = bookwellab_admin_script_vriable_data.payment_status_labels || {};
                    if (!status) return "";
                    return labels[status] || status;
                },

                bwab_getPaymentStatusTagType(status) {
                    if (status === "received") return "success";
                    if (status === "failed") return "danger";
                    if (status === "pending") return "warning";
                    return "info";
                },

                bwab_formatPaymentGateway(gateway) {
                    if (!gateway || gateway === "-") {
                        return "-";
                    }
                    const options = bookwellab_admin_script_vriable_data.payment_gateway_options || [];
                    const match = options.find((item) => item.value === gateway);
                    if (match && match.label) return match.label;
                    return gateway || "-";
                },

                bwab_getPaymentTransactionId(row) {
                    if (!row) {
                        return "";
                    }
                    return String(row.bw_payment_transaction_id || row.bw_wc_transaction_id || "").trim();
                },
                
                bwab_formatTime(timeStr) {
                    if(!timeStr) return "";
                    var format = bookwellab_admin_script_vriable_data.time_format || "H:i";
                    // Append dummy date to parse time
                    var d = new Date("1970-01-01T" + timeStr);
                    if(isNaN(d)) return timeStr;
                    
                    var options = {};
                    options.hour = "2-digit";
                    options.minute = "2-digit";
                    
                    if(format.includes("g") || format.includes("h")) {
                        options.hour12 = true;
                    } else {
                        options.hour12 = false;
                    }
                    
                    const locale = bookwellab_admin_script_vriable_data.calendar_locale || undefined;
                    return d.toLocaleTimeString(locale, options);
                },
                
                bwab_resetFilter() {
                    const vuedata = this;
                    vuedata.filter = JSON.parse(JSON.stringify(vuedata.filter_reset));
                    vuedata.bwab_getAppointments();
                },
                
                bwab_error_handler() {
                    const vuedata = this;
                    vuedata.$notify({
                        title: "Error",
                        message: "An error occurred fetching data",
                        type: "error",
                        customClass: "error_notification",
                        duration: 2200,
                        offset: vuedata.bwab_getNotificationOffset(),
                    });
                },
            ';

            return $bookwellab_page_method_data;
        }

        public function bookwellab_get_appointments_func()
        {
            global $wpdb;

            if (!\BookWellAB\BookWellAB_Permissions::has_permission('bookwellab_manage_appointments')) {
                wp_send_json_error(['msg' => esc_html__('Unauthorized', 'bookwellnow-appointment-booking')]);
            }

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json_error(['msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
            }

            $appt_table = esc_sql($this->bwdb->get_table_name('appointments'));
            $where = '1=1';
            $where_args = [];
            $filter = [];
            // BookWellAB_Input: scalars sanitized via map_deep; filter keys sanitized again below before query.
            $filter_raw = BookWellAB_Input::get_post_array('filter');
            if (!empty($filter_raw)) {
                $filter = [
                    'search' => isset($filter_raw['search']) ? sanitize_text_field($filter_raw['search']) : '',
                    'status' => isset($filter_raw['status']) ? sanitize_text_field($filter_raw['status']) : '',
                    'staff_id' => isset($filter_raw['staff_id']) ? absint($filter_raw['staff_id']) : 0,
                    'service_id' => isset($filter_raw['service_id']) ? absint($filter_raw['service_id']) : 0,
                    'date' => isset($filter_raw['date']) ? sanitize_text_field($filter_raw['date']) : '',
                ];
            }

            if (is_array($filter) && isset($filter['search']) && !empty($filter['search'])) {
                $s = '%' . $filter['search'] . '%';
                $where .= " AND (bw_customer_name LIKE %s OR bw_customer_email LIKE %s OR bw_customer_phone LIKE %s OR bw_appointment_id LIKE %s OR bw_staff_name LIKE %s OR bw_service_name LIKE %s)";
                array_push($where_args, $s, $s, $s, $s, $s, $s);
            }
            if (is_array($filter) && isset($filter['status']) && !empty($filter['status'])) {
                $where .= " AND bw_status = %s";
                $where_args[] = $filter['status'];
            }
            if (is_array($filter) && isset($filter['staff_id']) && !empty($filter['staff_id'])) {
                $where .= " AND bw_staff_id = %d";
                $where_args[] = $filter['staff_id'];
            }
            if (is_array($filter) && isset($filter['service_id']) && !empty($filter['service_id'])) {
                $where .= " AND bw_service_id = %d";
                $where_args[] = $filter['service_id'];
            }
            if (is_array($filter) && isset($filter['date']) && !empty($filter['date'])) {
                $where .= " AND bw_date = %s";
                $where_args[] = $filter['date'];
            }

            $limit = 10;
            $offset = 0;

            if (isset($_POST['limit']) && !empty($_POST['limit'])) {
                $limit = intval(wp_unslash($_POST['limit']));
            }

            if (isset($_POST['page']) && !empty($_POST['page'])) {
                $page = intval(wp_unslash($_POST['page']));
                $offset = ($page - 1) * $limit;
            }

            $limit = max(1, $limit);
            $offset = max(0, $offset);
            $sql = "SELECT SQL_CALC_FOUND_ROWS * FROM $appt_table WHERE $where ORDER BY bw_appointment_id DESC LIMIT %d OFFSET %d";
            $query_args = array_merge($where_args, [$limit, $offset]);
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and WHERE fragment are controlled by plugin logic; values are safely bound via prepare().
            $appointments = $wpdb->get_results($wpdb->prepare($sql, $query_args), ARRAY_A);

            $total_rows = $wpdb->get_var("SELECT FOUND_ROWS()");

            $appointments = apply_filters('bookwellab_appointments_list_data', is_array($appointments) ? $appointments : array());

            wp_send_json(['type' => 'success', 'list_data' => $appointments, 'total' => $total_rows]);
            exit;
        }

        public function bookwellab_update_appointment_func()
        {
            global $wpdb;

            if (!\BookWellAB\BookWellAB_Permissions::has_permission('bookwellab_manage_appointments')) {
                wp_send_json_error(['msg' => esc_html__('Unauthorized', 'bookwellnow-appointment-booking')]);
            }

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            // BookWellAB_Input: decoded JSON only; each field sanitized in $data array below before DB update.
            $appt_data = BookWellAB_Input::get_post_json('appt_data');
            $id = isset($appt_data['bw_appointment_id']) ? intval($appt_data['bw_appointment_id']) : 0;

            if ($id <= 0) {
                wp_send_json(['type' => 'error', 'msg' => 'Invalid ID']);
                exit;
            }

            $table = esc_sql($this->bwdb->get_table_name('appointments'));

            // Fetch old status before update
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $old_status = $wpdb->get_var($wpdb->prepare("SELECT bw_status FROM $table WHERE bw_appointment_id = %d", $id));
            $new_status = sanitize_text_field($appt_data['bw_status']);

            $data = [
                'bw_status' => $new_status,
                'bw_customer_name' => sanitize_text_field($appt_data['bw_customer_name']),
                'bw_customer_email' => sanitize_email($appt_data['bw_customer_email']),
                'bw_customer_phone_code' => sanitize_text_field($appt_data['bw_customer_phone_code']),
                'bw_customer_phone' => sanitize_text_field($appt_data['bw_customer_phone']),
                'bw_customer_country_slug' => isset($appt_data['bw_customer_country_slug']) ? sanitize_key($appt_data['bw_customer_country_slug']) : '',
                'bw_note' => sanitize_textarea_field($appt_data['bw_note']),
                'bw_payment_gateway' => sanitize_text_field($appt_data['bw_payment_gateway']),
                'bw_payment_status' => sanitize_text_field($appt_data['bw_payment_status']),
            ];

            $booking_update = $this->prepare_admin_booking_fields($appt_data, $id);
            if (is_wp_error($booking_update)) {
                wp_send_json(['type' => 'error', 'msg' => $booking_update->get_error_message()]);
                exit;
            }

            $selected_extras = array();
            if (!empty($booking_update)) {
                if (isset($booking_update['bw_extras'])) {
                    $selected_extras = is_array($booking_update['bw_extras']) ? $booking_update['bw_extras'] : array();
                    unset($booking_update['bw_extras']);
                }
                $data = array_merge($data, $booking_update);
            }

            $updated = $wpdb->update($table, $data, ['bw_appointment_id' => $id]);
            if ($updated === false) {
                wp_send_json([
                    'type' => 'error',
                    'msg' => esc_html__('Failed to update appointment.', 'bookwellnow-appointment-booking'),
                ]);
                exit;
            }

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $updated_appointment = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE bw_appointment_id = %d", $id), ARRAY_A);

            if (is_array($updated_appointment)) {
                /**
                 * Fires after an appointment is updated from the admin screen.
                 * Fired before notifications so integrations (e.g. Zoom) can prepare placeholders.
                 *
                 * @param int                  $id           Appointment ID.
                 * @param array<string, mixed> $booking_data Booking payload including extras.
                 * @param array<string, mixed> $appointment  Updated appointment row.
                 */
                do_action(
                    'bookwellab_admin_appointment_updated',
                    $id,
                    array(
                        'service_id' => isset($booking_update['bw_service_id']) ? (int) $booking_update['bw_service_id'] : 0,
                        'staff_id' => isset($booking_update['bw_staff_id']) ? (int) $booking_update['bw_staff_id'] : 0,
                        'extras' => $selected_extras,
                        'custom_fields' => (isset($appt_data['bw_custom_fields']) && is_array($appt_data['bw_custom_fields']))
                            ? $appt_data['bw_custom_fields']
                            : array(),
                    ),
                    $updated_appointment
                );

                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
                $updated_appointment = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE bw_appointment_id = %d", $id), ARRAY_A);
            }

            // Trigger notification if status has changed
            if ($old_status !== $new_status) {
                $trigger = '';
                switch ($new_status) {
                    case 'approved':
                        $trigger = 'on_approval';
                        break;
                    case 'pending':
                        $trigger = 'on_pending';
                        break;
                    case 'cancelled':
                        $trigger = 'on_cancellation';
                        break;
                }

                if (!empty($trigger) && is_array($updated_appointment)) {
                    \BookWellAB\BookWellAB_Notifications::get_instance()->send_notification($trigger, $updated_appointment);
                }
            }

            wp_send_json([
                'type' => 'success',
                'title' => 'Success',
                'msg' => 'Appointment updated successfully.',
                'appointment' => is_array($updated_appointment) ? $updated_appointment : [],
            ]);
            exit;
        }

        public function bookwellab_delete_appointment_func()
        {
            global $wpdb;

            if (!\BookWellAB\BookWellAB_Permissions::has_permission('bookwellab_manage_appointments')) {
                wp_send_json_error(['msg' => esc_html__('Unauthorized', 'bookwellnow-appointment-booking')]);
            }

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

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

            if ($id <= 0) {
                wp_send_json(['type' => 'error', 'msg' => 'Invalid ID']);
                exit;
            }

            $table = $this->bwdb->get_table_name('appointments');
            $appointment_before_delete = $this->bwdb->select(
                'row',
                '*',
                $table,
                'bw_appointment_id = %d',
                [$id]
            );
            if (is_array($appointment_before_delete) && !empty($appointment_before_delete['bw_appointment_id'])) {
                /**
                 * Fires before an appointment is permanently deleted in admin.
                 *
                 * @param int                  $id          Appointment ID.
                 * @param array<string, mixed> $appointment Appointment row.
                 */
                do_action('bookwellab_before_delete_appointment', $id, $appointment_before_delete);
            }

            $deleted = $wpdb->delete($table, ['bw_appointment_id' => $id], ['%d']);

            if ($deleted === false) {
                wp_send_json(['type' => 'error', 'msg' => 'Failed to delete appointment.']);
                exit;
            }

            if ((int) $deleted === 0) {
                wp_send_json(['type' => 'error', 'msg' => 'Appointment not found or already deleted.']);
                exit;
            }

            wp_send_json(['type' => 'success', 'msg' => 'Appointment deleted successfully.']);
            exit;
        }

        public function bookwellab_admin_get_month_availability_func()
        {


            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->verify_admin_appointment_request()) {
                return;
            }

            $year = absint(BookWellAB_Input::get_post_string('year'));
            $month = absint(BookWellAB_Input::get_post_string('month'));
            $staff_id = absint(BookWellAB_Input::get_post_string('staff_id'));
            $service_id = absint(BookWellAB_Input::get_post_string('service_id'));
            $exclude_appointment_id = absint(BookWellAB_Input::get_post_string('exclude_appointment_id'));

            if ($year <= 0 || $month <= 0 || $month > 12 || $staff_id <= 0 || $service_id <= 0) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Invalid request', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->staff_has_service_assignment($staff_id, $service_id)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Selected service is not assigned to this staff member.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            $this->set_admin_slot_extras_context(
                $service_id,
                $staff_id,
                BookWellAB_Input::get_post_json('extras')
            );

            $disabled_dates = BookWellAB_Booking_Helper::get_admin_month_availability(
                $staff_id,
                $service_id,
                $year,
                $month,
                $exclude_appointment_id
            );

            wp_send_json([
                'type' => 'success',
                'disabled_dates' => $disabled_dates,
            ]);
            exit;
        }

        public function bookwellab_admin_get_time_slots_func()
        {

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->verify_admin_appointment_request()) {
                return;
            }

            $date = BookWellAB_Input::get_post_string('date');
            $staff_id = absint(BookWellAB_Input::get_post_string('staff_id'));
            $service_id = absint(BookWellAB_Input::get_post_string('service_id'));
            $exclude_appointment_id = absint(BookWellAB_Input::get_post_string('exclude_appointment_id'));

            if (!$this->is_valid_date_ymd($date) || $staff_id <= 0 || $service_id <= 0) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Invalid request', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->staff_has_service_assignment($staff_id, $service_id)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Selected service is not assigned to this staff member.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            $this->set_admin_slot_extras_context(
                $service_id,
                $staff_id,
                BookWellAB_Input::get_post_json('extras')
            );

            $slots = BookWellAB_Booking_Helper::get_admin_time_slots($date, $staff_id, $service_id, $exclude_appointment_id);

            wp_send_json([
                'type' => 'success',
                'slots' => $slots,
            ]);
            exit;
        }

        public function bookwellab_create_appointment_func()
        {
            global $wpdb;

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->verify_admin_appointment_request()) {
                return;
            }

            $appt_data = BookWellAB_Input::get_post_json('appt_data');
            if (empty($appt_data) || !is_array($appt_data)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('No data received', 'bookwellnow-appointment-booking')]);
                exit;
            }

            $created = $this->insert_admin_appointment($appt_data);
            if (is_wp_error($created)) {
                wp_send_json(['type' => 'error', 'msg' => $created->get_error_message()]);
                exit;
            }

            wp_send_json([
                'type' => 'success',
                'title' => esc_html__('Success', 'bookwellnow-appointment-booking'),
                'msg' => esc_html__('Appointment created successfully.', 'bookwellnow-appointment-booking'),
                'appointment' => $created,
            ]);
            exit;
        }

        private function verify_admin_appointment_request()
        {
            if (!\BookWellAB\BookWellAB_Permissions::has_permission('bookwellab_manage_appointments')) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Unauthorized', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            return true;
        }

        private function is_valid_date_ymd($date)
        {
            if (!is_string($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
                return false;
            }
            $dt = \DateTime::createFromFormat('Y-m-d', $date);
            return $dt && $dt->format('Y-m-d') === $date;
        }

        private function normalize_time_his($time)
        {
            if (!is_string($time)) {
                return false;
            }
            $time = trim($time);
            if (preg_match('/^\d{1,2}:\d{2}$/', $time)) {
                $parts = explode(':', $time);
                $time = sprintf('%02d:%02d:00', (int) $parts[0], (int) $parts[1]);
            }
            if (!preg_match('/^\d{2}:\d{2}:\d{2}$/', $time)) {
                return false;
            }
            $dt = \DateTime::createFromFormat('H:i:s', $time);
            if (!$dt || $dt->format('H:i:s') !== $time) {
                return false;
            }
            return $time;
        }

        private function staff_has_service_assignment($staff_id, $service_id)
        {
            global $wpdb;

            $staff_id = absint($staff_id);
            $service_id = absint($service_id);
            if ($staff_id <= 0 || $service_id <= 0) {
                return false;
            }

            $staff_serv_table = esc_sql($this->bwdb->get_table_name('staff_services'));
            $count_sql = "SELECT COUNT(*) FROM {$staff_serv_table} WHERE bw_staff_id = %d AND bw_service_id = %d";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; values use prepare().
            $count = $wpdb->get_var($wpdb->prepare($count_sql, $staff_id, $service_id));

            return absint($count) > 0;
        }

        /**
         * @param array $appt_data Appointment payload.
         * @param int   $exclude_appointment_id Appointment ID to ignore for slot checks.
         * @return array|\WP_Error
         */
        private function prepare_admin_booking_fields($appt_data, $exclude_appointment_id = 0)
        {
            $staff_id = isset($appt_data['bw_staff_id']) ? absint($appt_data['bw_staff_id']) : 0;
            $service_id = isset($appt_data['bw_service_id']) ? absint($appt_data['bw_service_id']) : 0;
            $date = isset($appt_data['bw_date']) ? sanitize_text_field($appt_data['bw_date']) : '';
            $time_raw = isset($appt_data['bw_time']) ? sanitize_text_field($appt_data['bw_time']) : '';

            if ($staff_id <= 0 || $service_id <= 0 || $date === '' || $time_raw === '') {
                return new \WP_Error('missing_booking_fields', esc_html__('Staff, service, date, and time are required.', 'bookwellnow-appointment-booking'));
            }

            if (!$this->is_valid_date_ymd($date)) {
                return new \WP_Error('invalid_date', esc_html__('Invalid appointment date format.', 'bookwellnow-appointment-booking'));
            }

            $time = $this->normalize_time_his($time_raw);
            if ($time === false) {
                return new \WP_Error('invalid_time', esc_html__('Invalid appointment time format.', 'bookwellnow-appointment-booking'));
            }

            if (!$this->staff_has_service_assignment($staff_id, $service_id)) {
                return new \WP_Error('invalid_staff_service', esc_html__('Selected service is not assigned to this staff member.', 'bookwellnow-appointment-booking'));
            }

            $extras = $this->parse_admin_extras_payload($appt_data);
            $this->set_admin_slot_extras_context($service_id, $staff_id, $extras);

            if (!BookWellAB_Booking_Helper::is_slot_available($date, $time, $staff_id, $service_id, false, $exclude_appointment_id)) {
                return new \WP_Error('slot_unavailable', esc_html__('This time slot is not available. Please select another time.', 'bookwellnow-appointment-booking'));
            }

            $meta = $this->get_staff_service_booking_meta($staff_id, $service_id);
            if (is_wp_error($meta)) {
                return $meta;
            }

            $booking_context = array(
                'service_id' => $service_id,
                'staff_id' => $staff_id,
                'extras' => $extras,
                'booking_data' => array(
                    'extras' => $extras,
                ),
            );

            $validation = apply_filters('bookwellab_validate_admin_appointment_data', true, $appt_data, $booking_context);
            if (is_wp_error($validation)) {
                return $validation;
            }
            if ($validation === false) {
                return new \WP_Error('invalid_appointment', esc_html__('Appointment data is invalid.', 'bookwellnow-appointment-booking'));
            }

            $duration = (int) apply_filters(
                'bookwellab_booking_duration_minutes',
                (int) $meta['duration'],
                $service_id,
                $staff_id,
                $booking_context
            );
            $price = (float) apply_filters(
                'bookwellab_booking_price',
                (float) $meta['price'],
                $service_id,
                $staff_id,
                array('extras' => $extras)
            );

            return [
                'bw_staff_id' => $staff_id,
                'bw_service_id' => $service_id,
                'bw_date' => $date,
                'bw_time' => $time,
                'bw_duration' => $duration,
                'bw_price' => $price,
                'bw_staff_name' => $meta['staff_name'],
                'bw_service_name' => $meta['service_name'],
                'bw_extras' => $extras,
            ];
        }

        /**
         * @param array<string, mixed> $appt_data Appointment payload.
         * @return array<int, array<string, mixed>>
         */
        private function parse_admin_extras_payload($appt_data)
        {
            $raw = isset($appt_data['bw_extras']) ? $appt_data['bw_extras'] : array();
            if (!is_array($raw)) {
                $raw = array();
            }

            $extras = apply_filters('bookwellab_parse_admin_appointment_extras', $raw, $appt_data);

            return is_array($extras) ? $extras : array();
        }

        /**
         * @param int   $service_id Service ID.
         * @param int   $staff_id   Staff ID.
         * @param array $extras     Selected extras payload.
         * @return void
         */
        private function set_admin_slot_extras_context($service_id, $staff_id, $extras = array())
        {
            if (!is_array($extras)) {
                $extras = array();
            }

            $parsed = apply_filters(
                'bookwellab_parse_admin_appointment_extras',
                $extras,
                array(
                    'bw_service_id' => absint($service_id),
                    'bw_staff_id' => absint($staff_id),
                )
            );

            if (!is_array($parsed)) {
                $parsed = array();
            }

            BookWellAB_Booking_Helper::set_slot_booking_context(array(
                'service_id' => absint($service_id),
                'staff_id' => absint($staff_id),
                'extras' => $parsed,
                'booking_data' => array(
                    'extras' => $parsed,
                ),
            ));
        }

        /**
         * @param int $staff_id Staff ID.
         * @param int $service_id Service ID.
         * @return array|\WP_Error
         */
        private function get_staff_service_booking_meta($staff_id, $service_id)
        {
            global $wpdb;

            $staff_serv_table = esc_sql($this->bwdb->get_table_name('staff_services'));
            $service_info_sql = "SELECT bw_service_price, bw_service_duration_val, bw_service_duration_unit FROM $staff_serv_table WHERE bw_staff_id = %d AND bw_service_id = %d";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; values use prepare().
            $service_info = $wpdb->get_row($wpdb->prepare($service_info_sql, $staff_id, $service_id), ARRAY_A);

            if (!$service_info) {
                return new \WP_Error('invalid_staff_service', esc_html__('Selected service is not assigned to this staff member.', 'bookwellnow-appointment-booking'));
            }

            $price = isset($service_info['bw_service_price']) ? floatval($service_info['bw_service_price']) : 0;
            $duration = isset($service_info['bw_service_duration_val']) ? intval($service_info['bw_service_duration_val']) : 0;
            if (isset($service_info['bw_service_duration_unit']) && $service_info['bw_service_duration_unit'] === 'h') {
                $duration *= 60;
            }

            if ($duration <= 0) {
                $services_table = esc_sql($this->bwdb->get_table_name('services'));
                $service_defaults_sql = "SELECT bw_service_duration_val, bw_service_duration_unit FROM $services_table WHERE bw_service_id = %d";
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; values use prepare().
                $service_defaults = $wpdb->get_row($wpdb->prepare($service_defaults_sql, $service_id), ARRAY_A);
                if ($service_defaults) {
                    $duration = intval($service_defaults['bw_service_duration_val']);
                    if (isset($service_defaults['bw_service_duration_unit']) && $service_defaults['bw_service_duration_unit'] === 'h') {
                        $duration *= 60;
                    }
                }
            }

            if ($duration <= 0) {
                $duration = 30;
            }

            $services_table = esc_sql($this->bwdb->get_table_name('services'));
            $staffs_table = esc_sql($this->bwdb->get_table_name('staffs'));
            $service_name_sql = "SELECT bw_service_name FROM $services_table WHERE bw_service_id = %d";
            $staff_name_sql = "SELECT bw_staff_name FROM $staffs_table WHERE bw_staff_id = %d";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; values use prepare().
            $service_name = $wpdb->get_var($wpdb->prepare($service_name_sql, $service_id));
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; values use prepare().
            $staff_name = $wpdb->get_var($wpdb->prepare($staff_name_sql, $staff_id));

            return [
                'price' => $price,
                'duration' => $duration,
                'service_name' => is_string($service_name) ? $service_name : '',
                'staff_name' => is_string($staff_name) ? $staff_name : '',
            ];
        }

        /**
         * @param array $appt_data Appointment payload.
         * @return array|\WP_Error
         */
        private function insert_admin_appointment($appt_data)
        {
            global $wpdb;

            $customer_name = isset($appt_data['bw_customer_name']) ? sanitize_text_field($appt_data['bw_customer_name']) : '';
            $email_raw = isset($appt_data['bw_customer_email']) ? trim((string) $appt_data['bw_customer_email']) : '';
            $phone = isset($appt_data['bw_customer_phone']) ? preg_replace('/\D+/', '', (string) $appt_data['bw_customer_phone']) : '';
            $customer_id = isset($appt_data['bw_customer_id']) ? absint($appt_data['bw_customer_id']) : 0;

            if ($customer_name === '' || $email_raw === '' || !is_email($email_raw) || $phone === '') {
                return new \WP_Error('missing_customer', esc_html__('Customer name, email, and phone are required.', 'bookwellnow-appointment-booking'));
            }

            $booking_fields = $this->prepare_admin_booking_fields($appt_data, 0);
            if (is_wp_error($booking_fields)) {
                return $booking_fields;
            }

            $email = sanitize_email($email_raw);
            $phone_code = isset($appt_data['bw_customer_phone_code']) ? sanitize_text_field($appt_data['bw_customer_phone_code']) : '';
            $country_slug = BookWellAB_Input::sanitize_country_slug($appt_data['bw_customer_country_slug'] ?? '');
            $note = isset($appt_data['bw_note']) ? sanitize_textarea_field($appt_data['bw_note']) : '';
            $status = isset($appt_data['bw_status']) ? sanitize_text_field($appt_data['bw_status']) : 'pending';
            $payment_gateway = isset($appt_data['bw_payment_gateway']) ? sanitize_text_field($appt_data['bw_payment_gateway']) : 'manually';
            $payment_status = isset($appt_data['bw_payment_status']) ? sanitize_text_field($appt_data['bw_payment_status']) : 'pending';

            $cust_table = esc_sql($this->bwdb->get_table_name('customers'));

            if ($customer_id > 0) {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
                $existing_customer = $wpdb->get_row($wpdb->prepare("SELECT * FROM $cust_table WHERE bw_customer_id = %d", $customer_id), ARRAY_A);
                if (!$existing_customer) {
                    return new \WP_Error('invalid_customer', esc_html__('Selected customer was not found.', 'bookwellnow-appointment-booking'));
                }
                $wpdb->update($cust_table, [
                    'bw_first_name' => $customer_name,
                    'bw_phone_code' => $phone_code,
                    'bw_phone' => $phone,
                    'bw_country_slug' => $country_slug,
                    'bw_email' => $email,
                ], ['bw_customer_id' => $customer_id]);
            } else {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
                $existing_customer = $wpdb->get_row($wpdb->prepare("SELECT * FROM $cust_table WHERE bw_email = %s", $email), ARRAY_A);

                if ($existing_customer) {
                    $customer_id = (int) $existing_customer['bw_customer_id'];
                    $wpdb->update($cust_table, [
                        'bw_first_name' => $customer_name,
                        'bw_phone_code' => $phone_code,
                        'bw_phone' => $phone,
                        'bw_country_slug' => $country_slug,
                    ], ['bw_customer_id' => $customer_id]);
                } else {
                    $wpdb->insert($cust_table, [
                        'bw_first_name' => $customer_name,
                        'bw_last_name' => '',
                        'bw_email' => $email,
                        'bw_phone_code' => $phone_code,
                        'bw_phone' => $phone,
                        'bw_country_slug' => $country_slug,
                    ]);
                    $customer_id = (int) $wpdb->insert_id;
                }
            }

            $appt_table = esc_sql($this->bwdb->get_table_name('appointments'));
            $hash = wp_generate_password(20, false);
            $inserted = $wpdb->insert($appt_table, [
                'bw_appointment_hash' => $hash,
                'bw_service_id' => $booking_fields['bw_service_id'],
                'bw_staff_id' => $booking_fields['bw_staff_id'],
                'bw_customer_id' => $customer_id,
                'bw_date' => $booking_fields['bw_date'],
                'bw_time' => $booking_fields['bw_time'],
                'bw_duration' => $booking_fields['bw_duration'],
                'bw_price' => $booking_fields['bw_price'],
                'bw_status' => $status,
                'bw_customer_name' => $customer_name,
                'bw_customer_email' => $email,
                'bw_customer_phone_code' => $phone_code,
                'bw_customer_phone' => $phone,
                'bw_customer_country_slug' => $country_slug,
                'bw_note' => $note,
                'bw_service_name' => $booking_fields['bw_service_name'],
                'bw_staff_name' => $booking_fields['bw_staff_name'],
                'bw_payment_gateway' => $payment_gateway,
                'bw_payment_status' => $payment_status,
            ]);

            if ($inserted === false) {
                return new \WP_Error('insert_failed', esc_html__('Failed to create appointment.', 'bookwellnow-appointment-booking'));
            }

            $appointment_id = (int) $wpdb->insert_id;
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $appointment = $wpdb->get_row($wpdb->prepare("SELECT * FROM $appt_table WHERE bw_appointment_id = %d", $appointment_id), ARRAY_A);

            if (is_array($appointment)) {
                /**
                 * Fires after an appointment is created from the admin screen.
                 * Runs before notifications so Zoom join URLs are available in emails.
                 *
                 * @param int                  $appointment_id Appointment ID.
                 * @param array<string, mixed> $booking_data   Booking payload including extras.
                 * @param array<string, mixed> $options        Context options.
                 */
                do_action(
                    'bookwellab_appointment_created',
                    $appointment_id,
                    array(
                        'service_id' => (int) $booking_fields['bw_service_id'],
                        'staff_id' => (int) $booking_fields['bw_staff_id'],
                        'extras' => isset($booking_fields['bw_extras']) ? $booking_fields['bw_extras'] : array(),
                        'custom_fields' => (isset($appt_data['bw_custom_fields']) && is_array($appt_data['bw_custom_fields']))
                            ? $appt_data['bw_custom_fields']
                            : array(),
                    ),
                    array('source' => 'admin')
                );

                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
                $appointment = $wpdb->get_row($wpdb->prepare("SELECT * FROM $appt_table WHERE bw_appointment_id = %d", $appointment_id), ARRAY_A);
                $trigger = ($status === 'approved') ? 'on_approval' : 'on_pending';
                if (is_array($appointment)) {
                    \BookWellAB\BookWellAB_Notifications::get_instance()->send_notification($trigger, $appointment);
                }
            }

            return is_array($appointment) ? $appointment : [];
        }

        public function bookwellab_search_customers_func()
        {
            global $wpdb;


            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->verify_admin_appointment_request()) {
                return;
            }

            $search = BookWellAB_Input::get_post_string('search');
            if ($search === '') {
                wp_send_json(['type' => 'success', 'customers' => []]);
                exit;
            }

            $customer_table = esc_sql($this->bwdb->get_table_name('customers'));
            $like = '%' . $wpdb->esc_like($search) . '%';
            $phone_digits = preg_replace('/\D+/', '', $search);

            $where = '(bw_first_name LIKE %s OR bw_last_name LIKE %s OR bw_email LIKE %s OR bw_phone LIKE %s OR CONCAT(bw_first_name, \' \', bw_last_name) LIKE %s OR CONCAT(bw_phone_code, bw_phone) LIKE %s)';
            $where_args = [$like, $like, $like, $like, $like, $like];

            if ($phone_digits !== '' && $phone_digits !== $search) {
                $phone_like = '%' . $wpdb->esc_like($phone_digits) . '%';
                $where .= ' OR bw_phone LIKE %s';
                $where_args[] = $phone_like;
            }

            $list_sql = "SELECT bw_customer_id, bw_first_name, bw_last_name, bw_email, bw_phone_code, bw_phone, bw_country_slug
                         FROM $customer_table
                         WHERE $where
                         ORDER BY bw_customer_id DESC
                         LIMIT 20";

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and WHERE are plugin-controlled; values are bound via prepare().
            $customers = $wpdb->get_results($wpdb->prepare($list_sql, $where_args), ARRAY_A);

            wp_send_json([
                'type' => 'success',
                'customers' => is_array($customers) ? $customers : [],
            ]);
            exit;
        }

        public function bookwellab_quick_add_customer_func()
        {
            global $wpdb;

            if (!check_ajax_referer('bw_wp_nonce', '_wpnonce', false)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Security token mismatch.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            if (!$this->verify_admin_appointment_request()) {
                return;
            }

            $customer_data = BookWellAB_Input::get_post_json('customer_data');
            if (empty($customer_data) || !is_array($customer_data)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('No data received', 'bookwellnow-appointment-booking')]);
                exit;
            }

            $first_name = isset($customer_data['bw_first_name']) ? sanitize_text_field($customer_data['bw_first_name']) : '';
            $last_name = isset($customer_data['bw_last_name']) ? sanitize_text_field($customer_data['bw_last_name']) : '';
            $email = isset($customer_data['bw_email']) ? sanitize_email($customer_data['bw_email']) : '';
            $phone_code = isset($customer_data['bw_phone_code']) ? sanitize_text_field($customer_data['bw_phone_code']) : '';
            $phone = isset($customer_data['bw_phone']) ? preg_replace('/\D+/', '', (string) $customer_data['bw_phone']) : '';
            $country_slug = BookWellAB_Input::sanitize_country_slug($customer_data['bw_country_slug'] ?? '');

            if ($first_name === '' || $email === '' || !is_email($email)) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('First name and a valid email are required.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            $table = esc_sql($this->bwdb->get_table_name('customers'));
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $exists = $wpdb->get_var($wpdb->prepare("SELECT bw_customer_id FROM $table WHERE bw_email = %s", $email));
            if ($exists) {
                wp_send_json([
                    'type' => 'error',
                    'msg' => esc_html__('Customer with this email already exists.', 'bookwellnow-appointment-booking'),
                ]);
                exit;
            }

            $inserted = $wpdb->insert($table, [
                'bw_first_name' => $first_name,
                'bw_last_name' => $last_name,
                'bw_email' => $email,
                'bw_phone_code' => $phone_code,
                'bw_phone' => $phone,
                'bw_country_slug' => $country_slug,
            ]);

            if ($inserted === false) {
                wp_send_json(['type' => 'error', 'msg' => esc_html__('Failed to add customer.', 'bookwellnow-appointment-booking')]);
                exit;
            }

            $customer_id = (int) $wpdb->insert_id;
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from trusted plugin schema helper.
            $customer = $wpdb->get_row($wpdb->prepare("SELECT bw_customer_id, bw_first_name, bw_last_name, bw_email, bw_phone_code, bw_phone, bw_country_slug FROM $table WHERE bw_customer_id = %d", $customer_id), ARRAY_A);

            wp_send_json([
                'type' => 'success',
                'title' => esc_html__('Success', 'bookwellnow-appointment-booking'),
                'msg' => esc_html__('Customer added successfully.', 'bookwellnow-appointment-booking'),
                'customer' => is_array($customer) ? $customer : [],
            ]);
            exit;
        }

        /**
         * @return array<string, array{price: float, duration: int}>
         */
        private function get_staff_service_pricing_map()
        {
            global $wpdb;

            $staff_serv_table = esc_sql($this->bwdb->get_table_name('staff_services'));
            $services_table = esc_sql($this->bwdb->get_table_name('services'));

            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are escaped via esc_sql from plugin schema helper.
            $pricing_sql = "SELECT ss.bw_staff_id, ss.bw_service_id, ss.bw_service_price, ss.bw_service_duration_val, ss.bw_service_duration_unit,
                    s.bw_service_duration_val AS default_duration_val, s.bw_service_duration_unit AS default_duration_unit
             FROM {$staff_serv_table} ss
             LEFT JOIN {$services_table} s ON s.bw_service_id = ss.bw_service_id";
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Static query; table names are escaped via esc_sql from plugin schema helper.
            $rows = $wpdb->get_results($pricing_sql, ARRAY_A);

            $map = array();
            if (!is_array($rows)) {
                return $map;
            }

            foreach ($rows as $row) {
                $staff_id = absint($row['bw_staff_id'] ?? 0);
                $service_id = absint($row['bw_service_id'] ?? 0);
                if ($staff_id <= 0 || $service_id <= 0) {
                    continue;
                }

                $duration = absint($row['bw_service_duration_val'] ?? 0);
                $duration_unit = isset($row['bw_service_duration_unit']) ? (string) $row['bw_service_duration_unit'] : 'm';
                if ($duration <= 0) {
                    $duration = absint($row['default_duration_val'] ?? 0);
                    $duration_unit = isset($row['default_duration_unit']) ? (string) $row['default_duration_unit'] : 'm';
                }
                if ($duration_unit === 'h') {
                    $duration *= 60;
                }
                if ($duration <= 0) {
                    $duration = 30;
                }

                $map[$staff_id . '_' . $service_id] = array(
                    'price' => floatval($row['bw_service_price'] ?? 0),
                    'duration' => $duration,
                );
            }

            return $map;
        }
    }
}
// phpcs:enable
