<?php

namespace BookWellAB;

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

if (!class_exists('BookWellAB_Calendar')) {

    class BookWellAB_Calendar
    {

        private static $instance = null;
        private $bookwell_page = 'calendar';
        private $bwdb;
        private $bw_wp_nonce;

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

        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_calendar_admin_html', [$this, 'bookwellab_calendar_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('wp_ajax_bookwellab_get_calendar_events', [$this, 'bookwellab_get_calendar_events_func']);
            add_action('wp_ajax_bookwellab_get_calendar_date_events', [$this, 'bookwellab_get_calendar_date_events_func']);
            add_action('wp_ajax_bookwellab_get_calendar_day_view', [$this, 'bookwellab_get_calendar_day_view_func']);
        }

        public function bookwellab_calendar_admin_html_func()
        {
            require_once BOOKWELLAB_HTML_PATH . 'calendar_view.php';
        }

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

            $bookwellab_admin_script_vriable_data['calendar_view'] = 'day';
            $bookwellab_admin_script_vriable_data['current_date'] = wp_date('Y-m-d', null, wp_timezone());
            $bookwellab_admin_script_vriable_data['calendar_events'] = [];
            $bookwellab_admin_script_vriable_data['calendar_day_data'] = null;
            $bookwellab_admin_script_vriable_data['calendar_day_loader'] = false;
            $bookwellab_admin_script_vriable_data['selected_staff'] = 0;
            $bookwellab_admin_script_vriable_data['popover_loader'] = false;
            $bookwellab_admin_script_vriable_data['current_date_events'] = [];
            $bookwellab_admin_script_vriable_data['active_appointment_id'] = null;
            $bookwellab_admin_script_vriable_data['active_popover_date'] = null;
            $bookwellab_admin_script_vriable_data['day_strip_mobile'] = false;

            // Generate weekday labels based on Settings → General → Week Starts On
            $bookwellab_admin_script_vriable_data['days_of_week'] = \BookWellAB\BookWellAB_Settings::get_week_day_labels(true);
            $bookwellab_admin_script_vriable_data['week_starts_on'] = \BookWellAB\BookWellAB_Settings::get_week_starts_on();

            // 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 = [];
            $default_staff_id = 0;
            if (!empty($all_staff)) {
                foreach ($all_staff as $staff) {
                    $staff_id = (int) $staff['bw_staff_id'];
                    if ($default_staff_id <= 0) {
                        $default_staff_id = $staff_id;
                    }
                    $staff_list[] = [
                        'value' => $staff_id,
                        'label' => $staff['bw_staff_name'],
                    ];
                }
            }

            $bookwellab_admin_script_vriable_data['selected_staff'] = $default_staff_id;
            $bookwellab_admin_script_vriable_data['staff_list'] = $staff_list;

            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_syncDayStripLayout();
                vuedata._bwDayStripResizeHandler = function() {
                    vuedata.bwab_syncDayStripLayout();
                };
                window.addEventListener("resize", vuedata._bwDayStripResizeHandler);
                vuedata.bwab_reloadCalendar();
            ';

            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'));
            $error_title = esc_js(__('Error', 'bookwellnow-appointment-booking'));
            $load_events_error = esc_js(__('Failed to load events', 'bookwellnow-appointment-booking'));

            $bookwellab_page_method_data .= '
                bwab_fetchDateEvents(date, appointment_id) {
                    var ajax_url = bookwellab_ajax_obj.ajax_url;
                    const vuedata = this;
                    
                    vuedata.popover_loader = true;
                    vuedata.current_date_events = [];
                    vuedata.active_appointment_id = appointment_id || null;

                    var postData = {
                        action: "bookwellab_get_calendar_date_events",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        date: date,
                        staff_id: vuedata.selected_staff
                    };
                    
                    axios.post(ajax_url, Qs.stringify(postData))
                    .then(function(response) {
                        vuedata.popover_loader = false;
                        if (response.data.success) {
                            vuedata.current_date_events = response.data.data;
                            if (!vuedata.active_appointment_id && Array.isArray(vuedata.current_date_events) && vuedata.current_date_events.length > 0) {
                                vuedata.active_appointment_id = vuedata.current_date_events[0].bw_appointment_id;
                            }
                        } else {
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: response.data.data.msg || "' . $load_events_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                        vuedata.popover_loader = false;
                    });
                },

                bwab_openMonthDateAppointments(date, appointment_id) {
                    var hasSameDateOpen = this.active_popover_date === date;
                    this.active_popover_date = date;
                    this.active_appointment_id = appointment_id || null;

                    if (!hasSameDateOpen) {
                        this.bwab_fetchDateEvents(date, this.active_appointment_id);
                        return;
                    }

                    if (!Array.isArray(this.current_date_events) || this.current_date_events.length === 0) {
                        this.bwab_fetchDateEvents(date, this.active_appointment_id);
                    }
                },

                bwab_closeMonthDatePopover(date) {
                    if (this.active_popover_date === date) {
                        this.active_popover_date = null;
                    }
                },

                bwab_approveAppointment(appointment_id) {
                    window.location.href = "?page=bookwell-appointments";
                },

                bwab_editAppointment(appointment_id) {
                    window.location.href = "?page=bookwell-appointments"; 
                },

                bwab_changeCalendarView(viewType) {
                    this.calendar_view = viewType;
                    this.bwab_reloadCalendar();
                },

                bwab_onStaffChange() {
                    this.bwab_reloadCalendar();
                },

                bwab_reloadCalendar() {
                    const vuedata = this;
                    if (!vuedata.selected_staff) {
                        vuedata.bw_list_loader = "0";
                        vuedata.calendar_events = [];
                        vuedata.calendar_day_data = null;
                        return;
                    }
                    vuedata.bwab_getCalendarEvents().then(function() {
                        if (vuedata.calendar_view === "day") {
                            vuedata.bwab_getCalendarDayView();
                        }
                    });
                },

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

                    if (!vuedata.selected_staff) {
                        vuedata.bw_list_loader = "0";
                        vuedata.calendar_events = [];
                        return Promise.resolve();
                    }
                    
                    vuedata.bw_list_loader = "1";
                    
                    var postData = {
                        action: "bookwellab_get_calendar_events",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        current_date: vuedata.current_date,
                        view: vuedata.calendar_view,
                        staff_id: vuedata.selected_staff
                    };
                    
                    return axios.post(ajax_url, Qs.stringify(postData))
                    .then(function(response) {
                        vuedata.bw_list_loader = "0";
                        if (response.data.success) {
                            vuedata.calendar_events = response.data.data;
                        } else {
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: response.data.data.msg || "' . $load_events_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                        vuedata.bw_list_loader = "0";
                    });
                },

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

                    if (!vuedata.selected_staff || vuedata.calendar_view !== "day") {
                        return;
                    }

                    vuedata.calendar_day_loader = true;

                    var postData = {
                        action: "bookwellab_get_calendar_day_view",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        date: vuedata.current_date,
                        staff_id: vuedata.selected_staff
                    };

                    axios.post(ajax_url, Qs.stringify(postData))
                    .then(function(response) {
                        vuedata.calendar_day_loader = false;
                        if (response.data.success) {
                            vuedata.calendar_day_data = response.data.data;
                        } else {
                            vuedata.calendar_day_data = null;
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: response.data.data.msg || "' . $load_events_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                        vuedata.calendar_day_loader = false;
                    });
                },

                bwab_goToday() {
                    this.current_date = new Date().toISOString().split("T")[0];
                    this.bwab_reloadCalendar();
                },

                bwab_shiftCalendarMonth(delta) {
                    let d = new Date(this.current_date);
                    d.setDate(1);
                    d.setMonth(d.getMonth() + delta);
                    this.current_date = d.toISOString().split("T")[0];
                    this.bwab_reloadCalendar();
                },

                bwab_selectCalendarDay(dateStr) {
                    if (!dateStr || this.current_date === dateStr) {
                        return;
                    }
                    this.current_date = dateStr;
                    this.bwab_reloadCalendar();
                },

                bwab_getDayDateStrip() {
                    let d = new Date(this.current_date);
                    let year = d.getFullYear();
                    let month = d.getMonth();
                    let lastDay = new Date(year, month + 1, 0).getDate();
                    let todayStr = new Date().toISOString().split("T")[0];
                    let strip = [];

                    for (let i = 1; i <= lastDay; i++) {
                        let dayDate = new Date(year, month, i, 12, 0, 0);
                        let dateStr = dayDate.toISOString().split("T")[0];
                        let dayEvents = this.calendar_events.filter(function(e) {
                            return e.bw_date === dateStr;
                        });
                        strip.push({
                            date: dateStr,
                            day: i,
                            weekday: dayDate.toLocaleString("default", { weekday: "narrow" }),
                            isSelected: dateStr === this.current_date,
                            isToday: dateStr === todayStr,
                            isPast: dateStr < todayStr,
                            hasEvents: dayEvents.length > 0
                        });
                    }

                    return strip;
                },

                bwab_syncDayStripLayout() {
                    let isMobile = window.innerWidth <= 782;
                    if (this.day_strip_mobile !== isMobile) {
                        this.day_strip_mobile = isMobile;
                    }
                },

                bwab_getDayDateStripWeekRows(strip) {
                    if (!strip.length) {
                        return [];
                    }

                    let d = new Date(this.current_date);
                    let year = d.getFullYear();
                    let month = d.getMonth();
                    let firstDay = new Date(year, month, 1);
                    let weekStartsOn = Number(bookwellab_admin_script_vriable_data.week_starts_on);
                    if (isNaN(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) {
                        weekStartsOn = 1;
                    }
                    let startDayIndex = (firstDay.getDay() - weekStartsOn + 7) % 7;

                    let rows = [];
                    let currentRow = [];
                    let rowIndex = 0;

                    for (let i = 0; i < startDayIndex; i++) {
                        currentRow.push(null);
                    }

                    for (let i = 0; i < strip.length; i++) {
                        currentRow.push(strip[i]);
                        if (currentRow.length === 7) {
                            rows.push({ key: "week-" + rowIndex, days: currentRow.slice() });
                            rowIndex++;
                            currentRow = [];
                        }
                    }

                    if (currentRow.length > 0) {
                        while (currentRow.length < 7) {
                            currentRow.push(null);
                        }
                        rows.push({ key: "week-" + rowIndex, days: currentRow.slice() });
                    }

                    return rows;
                },

                bwab_getDayDateStripRows() {
                    let strip = this.bwab_getDayDateStrip();

                    if (this.day_strip_mobile) {
                        return this.bwab_getDayDateStripWeekRows(strip);
                    }

                    let cols = 16;
                    let row1 = strip.slice(0, cols);
                    let row2 = strip.slice(cols);

                    while (row2.length < cols) {
                        row2.push(null);
                    }

                    return [
                        { key: "row1", days: row1 },
                        { key: "row2", days: row2 }
                    ];
                },

                bwab_getDayViewDisplayDate() {
                    let d = new Date(this.current_date);
                    const locale = bookwellab_admin_script_vriable_data.calendar_locale || undefined;
                    return d.toLocaleString(locale, { month: "long", day: "numeric" });
                },

                bwab_getDayViewPxPerMinute() {
                    let hourHeight = (this.calendar_day_data && this.calendar_day_data.hour_height)
                        ? this.calendar_day_data.hour_height
                        : 80;
                    return hourHeight / 60;
                },

                bwab_getDayViewTimeSlots() {
                    if (!this.calendar_day_data || !this.calendar_day_data.timeline) {
                        return [];
                    }
                    return this.calendar_day_data.timeline.hours || [];
                },

                bwab_getDayViewEventStyle(event) {
                    if (!this.calendar_day_data || !this.calendar_day_data.timeline) {
                        return {};
                    }
                    let startMinutes = this.calendar_day_data.timeline.start_minutes || 0;
                    let pxPerMinute = this.bwab_getDayViewPxPerMinute();
                    let top = ((event.start_minutes || 0) - startMinutes) * pxPerMinute;
                    let height = ((event.end_minutes || 0) - (event.start_minutes || 0)) * pxPerMinute;
                    if (height < 32) {
                        height = 32;
                    }
                    return {
                        top: top + "px",
                        height: height + "px"
                    };
                },

                bwab_getDayViewNowStyle() {
                    if (!this.calendar_day_data || this.calendar_day_data.now_top === null || this.calendar_day_data.now_top === undefined) {
                        return { display: "none" };
                    }
                    return {
                        top: this.calendar_day_data.now_top + "px"
                    };
                },
                
                bwab_prevMonth() {
                    let d = new Date(this.current_date);
                    if(this.calendar_view === "month") {
                        d.setMonth(d.getMonth() - 1);
                    } else if(this.calendar_view === "week") {
                        d.setDate(d.getDate() - 7);
                    } else if(this.calendar_view === "day") {
                        d.setDate(d.getDate() - 1);
                    }
                    this.current_date = d.toISOString().split("T")[0];
                    this.bwab_reloadCalendar();
                },
                
                bwab_nextMonth() {
                    let d = new Date(this.current_date);
                    if(this.calendar_view === "month") {
                        d.setMonth(d.getMonth() + 1);
                    } else if(this.calendar_view === "week") {
                        d.setDate(d.getDate() + 7);
                    } else if(this.calendar_view === "day") {
                        d.setDate(d.getDate() + 1);
                    }
                    this.current_date = d.toISOString().split("T")[0];
                    this.bwab_reloadCalendar();
                },
                
                bwab_getDisplayMonth() {
                     let d = new Date(this.current_date);
                     const locale = bookwellab_admin_script_vriable_data.calendar_locale || undefined;
                     return d.toLocaleString(locale, { month: "long", year: "numeric" });
                },
                
                bwab_getCalendarGrid() {
                     let d = new Date(this.current_date);
                     let year = d.getFullYear();
                     let month = d.getMonth();
                     
                     let firstDay = new Date(year, month, 1);
                     let lastDay = new Date(year, month + 1, 0);
                     
                     let weekStartsOn = Number(bookwellab_admin_script_vriable_data.week_starts_on);
                     if (isNaN(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) {
                         weekStartsOn = 1;
                     }
                     let startDayIndex = (firstDay.getDay() - weekStartsOn + 7) % 7;
                     
                     let grid = [];
                     
                     // previous month padding
                     let prevMonthLastDay = new Date(year, month, 0).getDate();
                     for(let i = startDayIndex - 1; i >= 0; i--) {
                         let dayNum = prevMonthLastDay - i;
                         grid.push({ date: new Date(year, month - 1, dayNum).toISOString().split("T")[0], day: dayNum, isCurrentMonth: false, events: [] });
                     }
                     
                     // current month days
                     for(let i = 1; i <= lastDay.getDate(); i++) {
                         let dateStr = new Date(year, month, i, 12, 0, 0).toISOString().split("T")[0];
                         let dayEvents = this.calendar_events.filter(e => e.bw_date === dateStr);
                         grid.push({ date: dateStr, day: i, isCurrentMonth: true, events: dayEvents });
                     }
                     
                     // next month padding to complete 42 cells (6 rows)
                     let remaining = 42 - grid.length;
                     for(let i = 1; i <= remaining; i++) {
                         grid.push({ date: new Date(year, month + 1, i).toISOString().split("T")[0], day: i, isCurrentMonth: false, events: [] });
                     }
                     
                     return grid;
                },
                
                bwab_getTimeSlots() {
                    var slots = [];
                    for(let i=0; i<24; i++) {
                        let h = i % 12 || 12;
                        var ampm = i < 12 ? "am" : "pm";
                        var displayVal = h.toString().padStart(2, "0") + ":00 " + ampm;
                        slots.push({ val: i, label: displayVal });
                    }
                    return slots;
                },

                bwab_getWeekGrid() {
                    let d = new Date(this.current_date);
                    let weekStartsOn = Number(bookwellab_admin_script_vriable_data.week_starts_on);
                    if (isNaN(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) {
                        weekStartsOn = 1;
                    }
                    let day = d.getDay();
                    let offset = (day - weekStartsOn + 7) % 7;
                    d.setDate(d.getDate() - offset);

                    let weekVars = [];
                    for(let i=0; i<7; i++) {
                        let curD = new Date(d);
                        curD.setDate(d.getDate() + i);
                        let dateStr = curD.toISOString().split("T")[0];
                        
                        var dayName = curD.toLocaleString("default", { weekday: "short" });
                        var dayNum = curD.getDate();

                        var dayEvents = this.calendar_events.filter(e => e.bw_date === dateStr);
                        
                        weekVars.push({
                            label: dayName + " " + dayNum,
                            date: dateStr,
                            events: dayEvents
                        });
                    }
                    return weekVars;
                },

                bwab_getDayGrid() {
                    let d = new Date(this.current_date);
                    var dateStr = d.toISOString().split("T")[0];
                    var dayName = d.toLocaleString("default", { weekday: "long" });
                    var dayNum = d.getDate();

                    var dayEvents = this.calendar_events.filter(e => e.bw_date === dateStr);
                    
                    return [{
                        label: dayName + " " + dayNum,
                        date: dateStr,
                        events: dayEvents
                    }];
                },
                
                bwab_getEventStyle(event) {
                    // Extract start hour/min
                    let tSplit = event.bw_time.split(":");
                    let startH = parseInt(tSplit[0]);
                    let startM = parseInt(tSplit[1] || 0);

                    // Handle end time implicitly if not provided +30mins
                    let endH = startH;
                    let endM = startM + 30;
                    if (event.bw_end_time) {
                        let etSplit = event.bw_end_time.split(":");
                        endH = parseInt(etSplit[0]);
                        endM = parseInt(etSplit[1] || 0);
                    } else if (endM >= 60) {
                        endH++;
                        endM -= 60;
                    }

                    let startOffset = (startH * 60) + startM;
                    let endOffset = (endH * 60) + endM;
                    let duration = endOffset - startOffset;

                    // Each hour is logically 60px (or determined by CSS)
                    // Let\'s assume 1hr = 60px height in our grid
                    return {
                        top: startOffset + "px",
                        height: duration + "px"
                    };
                },

                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";
                    
                    if(format == "Y-m-d") return dateStr; 

                    return d.toLocaleDateString(undefined, options);
                },
                
                bwab_formatTime(timeStr) {
                    if(!timeStr) return "";
                    var format = bookwellab_admin_script_vriable_data.time_format || "H:i";
                    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;
                    }
                    
                    return d.toLocaleTimeString([], options);
                },

                bwab_addAppointment() {
                    window.location.href = "?page=bookwell-appointments"; 
                }
            ';

            return $bookwellab_page_method_data;
        }

        public function bookwellab_get_calendar_events_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')]);
            }

            $staff_id = $this->resolve_calendar_staff_id_from_request();
            if ($staff_id <= 0) {
                wp_send_json_error(['msg' => esc_html__('Please select a staff member.', 'bookwellnow-appointment-booking')]);
            }

            $current_date = BookWellAB_Input::get_post_string('current_date');
            $current_date = ($current_date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $current_date))
                ? $current_date
                : wp_date('Y-m-d', null, wp_timezone());

            $d = new \DateTime($current_date);
            $start_date = $d->modify('first day of this month')->format('Y-m-d');
            $end_date = $d->modify('last day of this month')->format('Y-m-d');

            $d_start = new \DateTime($start_date);
            $d_start->modify('-7 days');
            $from = $d_start->format('Y-m-d');

            $d_end = new \DateTime($end_date);
            $d_end->modify('+7 days');
            $to = $d_end->format('Y-m-d');

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

            $sql = "SELECT * FROM $appt_table WHERE bw_date >= %s AND bw_date <= %s AND bw_staff_id = %d ORDER BY bw_time ASC";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is trusted, query fragments are controlled, and values are bound via prepare().
            $events = $wpdb->get_results($wpdb->prepare($sql, $from, $to, $staff_id), ARRAY_A);

            wp_send_json_success($events);
        }

        public function bookwellab_get_calendar_date_events_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')]);
            }

            $staff_id = $this->resolve_calendar_staff_id_from_request();
            if ($staff_id <= 0) {
                wp_send_json_error(['msg' => esc_html__('Please select a staff member.', 'bookwellnow-appointment-booking')]);
            }

            $date = BookWellAB_Input::get_post_string('date');
            if ($date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
                wp_send_json_error(['msg' => esc_html__('Invalid Date.', 'bookwellnow-appointment-booking')]);
            }

            $appt_table = esc_sql($this->bwdb->get_table_name('appointments'));
            $service_table = esc_sql($this->bwdb->get_table_name('services'));

            $sql = "SELECT a.*, s.bw_service_name as service_name 
                 FROM $appt_table a 
                 LEFT JOIN $service_table s ON a.bw_service_id = s.bw_service_id 
                 WHERE a.bw_date = %s AND a.bw_staff_id = %d
                 ORDER BY a.bw_time ASC";

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names are trusted, query fragments are controlled, and values are bound via prepare().
            $events = $wpdb->get_results($wpdb->prepare($sql, $date, $staff_id), ARRAY_A);

            wp_send_json_success($events);
        }

        /**
         * Pixel height for one hour row in the calendar day view.
         *
         * @return int
         */
        private function get_calendar_hour_height_px()
        {
            return 80;
        }

        public function bookwellab_get_calendar_day_view_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')]);
            }

            $staff_id = $this->resolve_calendar_staff_id_from_request();
            if ($staff_id <= 0) {
                wp_send_json_error(['msg' => esc_html__('Please select a staff member.', 'bookwellnow-appointment-booking')]);
            }

            $date_raw = BookWellAB_Input::get_post_string('date');
            $date = ($date_raw !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_raw))
                ? $date_raw
                : wp_date('Y-m-d', null, wp_timezone());

            $staff_table = esc_sql($this->bwdb->get_table_name('staffs'));
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is plugin-controlled.
            $staff_row = $wpdb->get_row($wpdb->prepare("SELECT bw_staff_id, bw_staff_name, bw_staff_image FROM $staff_table WHERE bw_staff_id = %d", $staff_id), ARRAY_A);
            if (empty($staff_row)) {
                wp_send_json_error(['msg' => esc_html__('Staff not found.', 'bookwellnow-appointment-booking')]);
            }

            $staff_image = !empty($staff_row['bw_staff_image']) ? $staff_row['bw_staff_image'] : BookWellAB_Frontend::get_default_staff_image_url();
            $schedule = BookWellAB_Booking_Helper::get_staff_day_schedule($staff_id, $date);

            $appt_table = esc_sql($this->bwdb->get_table_name('appointments'));
            $service_table = esc_sql($this->bwdb->get_table_name('services'));
            $appointments_sql = "SELECT a.*, s.bw_service_name as service_name
                FROM $appt_table a
                LEFT JOIN $service_table s ON a.bw_service_id = s.bw_service_id
                WHERE a.bw_date = %s AND a.bw_staff_id = %d
                AND (a.bw_status IS NULL OR LOWER(TRIM(a.bw_status)) NOT IN ('cancelled', 'canceled'))
                ORDER BY a.bw_time ASC";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names are plugin-controlled; values use prepare().
            $appointment_rows = $wpdb->get_results($wpdb->prepare($appointments_sql, $date, $staff_id), ARRAY_A);

            $timeline = BookWellAB_Booking_Helper::build_staff_day_timeline($schedule, $appointment_rows, $date);
            $appointments = BookWellAB_Booking_Helper::format_staff_day_appointments($appointment_rows, $date, $timeline);
            $now_indicator = BookWellAB_Booking_Helper::get_day_timeline_now_indicator($date, $timeline);
            $hour_height = $this->get_calendar_hour_height_px();
            $px_per_minute = $hour_height / 60;
            $now_top = null;
            if ($now_indicator['now_top'] !== null) {
                $now_top = (int) round($now_indicator['now_top'] * $px_per_minute);
            }

            wp_send_json_success([
                'staff' => [
                    'id' => $staff_id,
                    'name' => (string) ($staff_row['bw_staff_name'] ?? ''),
                    'image' => esc_url($staff_image),
                ],
                'timeline' => $timeline,
                'appointments' => $appointments,
                'is_off' => !empty($schedule['is_off']),
                'now_top' => $now_top,
                'now_label' => $now_indicator['now_label'],
                'hour_height' => $hour_height,
            ]);
        }

        /**
         * Resolve staff ID from AJAX request.
         *
         * @return int
         */
        private function resolve_calendar_staff_id_from_request()
        {
            return absint(BookWellAB_Input::get_post_string('staff_id'));
        }

    }

}
// phpcs:enable
