<?php

namespace BookWellAB;

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

if (!class_exists('BookWellAB_Dashboard')) {

    class BookWellAB_Dashboard
    {

        private static $instance = null;
        private $bookwell_page = 'bookwell';
        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()
                : '';

            // Note: The main dashboard page slug is 'bookwell', which results in $current_page being 'bookwell' 
            // because of str_replace('bookwell-', '', 'bookwell') in class.bookwell.php returning 'bookwell'.

            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_bookwell_admin_html', [$this, 'bookwellab_dashboard_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('bookwellab_add_extra_admin_assets', [$this, 'bookwellab_add_dashboard_assets']);
            }

            add_action('wp_ajax_bookwellab_get_dashboard_counts', [$this, 'bookwellab_get_dashboard_counts_func']);
            add_action('wp_ajax_bookwellab_get_dashboard_chart', [$this, 'bookwellab_get_dashboard_chart_func']);
            add_action('wp_ajax_bookwellab_get_dashboard_top_services', [$this, 'bookwellab_get_dashboard_top_services_func']);
            add_action('wp_ajax_bookwellab_get_dashboard_day_preview', [$this, 'bookwellab_get_dashboard_day_preview_func']);
        }

        public function bookwellab_add_dashboard_assets()
        {
            wp_register_style(
                'bookwellab-dashboard-style',
                plugin_dir_url(BOOKWELLAB_FILE) . 'assets/css/dashboard.css',
                ['bookwellab-admin-style', 'bookwellab-elements-style'],
                BOOKWELLAB_VERSION
            );
            wp_enqueue_style('bookwellab-dashboard-style');

            wp_add_inline_style(
                'bookwellab-dashboard-style',
                'body.bookwellab-admin-page .bw-dashboard-page,body.bookwellab-admin-page .bw-dashboard-page .container,body.bookwellab-admin-page .bw-dashboard-container{background:#f6f5fa!important;}'
            );

            wp_enqueue_script('bookwellab-chartjs', plugin_dir_url(BOOKWELLAB_FILE) . 'assets/js/chart.umd.min.js', [], BOOKWELLAB_VERSION, true);
        }

        public function bookwellab_dashboard_admin_html_func()
        {
            require_once BOOKWELLAB_HTML_PATH . 'dashboard_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['dashboard_filter'] = 'this_month';
            $bookwellab_admin_script_vriable_data['dashboard_custom_date'] = [];
            $bookwellab_admin_script_vriable_data['dashboard_data'] = [
                'total_appointments' => 0,
                'pending_appointments' => 0,
                'approved_appointments' => 0,
                'cancelled_appointments' => 0,
                'new_customers' => 0,
                'total_customers' => 0,
                'received_payments' => 0,
                'pending_payments' => 0,
                'revenue_chart' => [
                    'labels' => [],
                    'received_values' => [],
                    'pending_values' => [],
                ],
                'top_services' => [],
            ];
            $bookwellab_admin_script_vriable_data['dashboard_chart_loading'] = false;

            $day_preview_filters = $this->get_day_preview_filter_options();
            $bookwellab_admin_script_vriable_data['day_preview_date'] = wp_date('Y-m-d', null, wp_timezone());
            $bookwellab_admin_script_vriable_data['day_preview_staff_id'] = $day_preview_filters['default_staff_id'];
            $bookwellab_admin_script_vriable_data['day_preview_service_id'] = 'all';
            $bookwellab_admin_script_vriable_data['day_preview_view'] = 'appointments';
            $bookwellab_admin_script_vriable_data['day_preview_staff_list'] = $day_preview_filters['staff_list'];
            $bookwellab_admin_script_vriable_data['day_preview_service_list'] = $day_preview_filters['service_list'];
            $bookwellab_admin_script_vriable_data['day_preview_data'] = [
                'staff' => null,
                'timeline' => null,
                'appointments' => [],
                'availability' => [],
                'is_off' => false,
                'now_percent' => null,
                'now_label' => '',
            ];
            $bookwellab_admin_script_vriable_data['day_preview_loading'] = false;
            $bookwellab_admin_script_vriable_data['day_preview_hovered_appt'] = null;

            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_getDashboardData();
                vuedata.bwab_getDayPreview();
            ';

            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_counts_error = esc_js(__('Failed to load dashboard counts', 'bookwellnow-appointment-booking'));
            $load_chart_error = esc_js(__('Failed to load revenue chart', 'bookwellnow-appointment-booking'));
            $load_top_services_error = esc_js(__('Failed to load most booked services', 'bookwellnow-appointment-booking'));
            $load_day_preview_error = esc_js(__('Failed to load day preview', 'bookwellnow-appointment-booking'));
            $received_payments_label = esc_js(__('Received Payments', 'bookwellnow-appointment-booking'));
            $pending_payments_label = esc_js(__('Pending Payments', 'bookwellnow-appointment-booking'));

            $bookwellab_page_method_data .= '
                bwab_getDashboardFilterPostData(action) {
                    return {
                        action: action,
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        filter: this.dashboard_filter,
                        custom_date: this.dashboard_custom_date
                    };
                },
                bwab_getDashboardData() {
                    this.bwab_getDashboardCounts();
                    this.bwab_getDashboardChart();
                    this.bwab_getDashboardTopServices();
                },
                bwab_getDashboardCounts() {
                    const vuedata = this;
                    axios.post(bookwellab_ajax_obj.ajax_url, Qs.stringify(vuedata.bwab_getDashboardFilterPostData("bookwellab_get_dashboard_counts")))
                    .then(function(response) {
                        if (response.data.success && response.data.data) {
                            Object.assign(vuedata.dashboard_data, response.data.data);
                        } else {
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: (response.data.data && response.data.data.msg) ? response.data.data.msg : "' . $load_counts_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                    });
                },
                bwab_getDashboardChart() {
                    const vuedata = this;
                    vuedata.dashboard_chart_loading = true;
                    axios.post(bookwellab_ajax_obj.ajax_url, Qs.stringify(vuedata.bwab_getDashboardFilterPostData("bookwellab_get_dashboard_chart")))
                    .then(function(response) {
                        vuedata.dashboard_chart_loading = false;
                        if (response.data.success && response.data.data && response.data.data.revenue_chart) {
                            vuedata.dashboard_data.revenue_chart = response.data.data.revenue_chart;
                            vuedata.$nextTick(function() {
                                vuedata.bwab_renderChart();
                            });
                        } else {
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: (response.data.data && response.data.data.msg) ? response.data.data.msg : "' . $load_chart_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                        vuedata.dashboard_chart_loading = false;
                    });
                },
                bwab_getDashboardTopServices() {
                    const vuedata = this;
                    axios.post(bookwellab_ajax_obj.ajax_url, Qs.stringify(vuedata.bwab_getDashboardFilterPostData("bookwellab_get_dashboard_top_services")))
                    .then(function(response) {
                        if (response.data.success && response.data.data) {
                            vuedata.dashboard_data.top_services = response.data.data.top_services || [];
                            vuedata.bwab_applyTopServiceDayPreviewDefault();
                        } else {
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: (response.data.data && response.data.data.msg) ? response.data.data.msg : "' . $load_top_services_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                    });
                },
                bwab_getDayPreviewPostData() {
                    return {
                        action: "bookwellab_get_dashboard_day_preview",
                        _wpnonce: bookwellab_ajax_obj.bookwellab_nonce,
                        date: this.day_preview_date,
                        staff_id: this.day_preview_staff_id,
                        service_id: this.day_preview_service_id,
                        view: this.day_preview_view
                    };
                },
                bwab_applyTopServiceDayPreviewDefault() {
                    if (this.day_preview_service_id !== "all") {
                        return;
                    }
                    const top = this.dashboard_data.top_services;
                    if (!top || !top.length) {
                        return;
                    }
                    const first = top[0];
                    if (first && first.bw_service_id) {
                        this.day_preview_service_id = parseInt(first.bw_service_id, 10);
                        this.bwab_getDayPreview();
                    }
                },
                bwab_getDayPreview() {
                    const vuedata = this;
                    if (!vuedata.day_preview_staff_id) {
                        return;
                    }
                    vuedata.day_preview_loading = true;
                    axios.post(bookwellab_ajax_obj.ajax_url, Qs.stringify(vuedata.bwab_getDayPreviewPostData()))
                    .then(function(response) {
                        vuedata.day_preview_loading = false;
                        if (response.data.success && response.data.data) {
                            vuedata.day_preview_data = response.data.data;
                        } else {
                            vuedata.$notify({
                                title: "' . $error_title . '",
                                message: (response.data.data && response.data.data.msg) ? response.data.data.msg : "' . $load_day_preview_error . '",
                                type: "error"
                            });
                        }
                    }).catch(function(error) {
                        console.log(error);
                        vuedata.day_preview_loading = false;
                    });
                },
                bwab_dayPreviewTrackWidthStyle() {
                    if (typeof window === "undefined" || window.innerWidth > 782) {
                        return {};
                    }
                    const timeline = this.day_preview_data && this.day_preview_data.timeline;
                    if (!timeline || !timeline.hours || !timeline.hours.length) {
                        return {};
                    }
                    const minWidth = Math.max(timeline.hours.length * 68, 340);
                    return {
                        minWidth: minWidth + "px",
                        flex: "1 0 " + minWidth + "px"
                    };
                },
                bwab_dayPreviewSegStyle(seg) {
                    const timeline = this.day_preview_data && this.day_preview_data.timeline;
                    if (!timeline || !seg) {
                        return {};
                    }
                    const startMinutes = Number(seg.start_minutes);
                    const endMinutes = Number(seg.end_minutes);
                    const timelineStart = Number(timeline.start_minutes);
                    const timelineEnd = Number(timeline.end_minutes);
                    const range = timelineEnd - timelineStart;
                    if (range <= 0 || !Number.isFinite(startMinutes) || !Number.isFinite(endMinutes)) {
                        return {};
                    }
                    const left = ((startMinutes - timelineStart) / range) * 100;
                    const width = ((endMinutes - startMinutes) / range) * 100;
                    return {
                        left: Math.max(0, left) + "%",
                        width: Math.max(0.5, width) + "%"
                    };
                },
                bwab_formatDayPreviewRange(appt) {
                    if (!appt || !appt.time_label) {
                        return "";
                    }
                    return appt.date_label + ", " + appt.time_label;
                },
                
                bwab_renderChart() {
                    const vuedata = this;
                    const ctx = document.getElementById("revenueChart");
                    
                    if(!ctx) return;
                    
                    if(window.myRevenueChart) {
                        window.myRevenueChart.destroy();
                    }
                    
                    const chartData = vuedata.dashboard_data.revenue_chart;
                    
                    var chartCtx = ctx.getContext("2d");
                    var chartHeight = ctx.parentNode ? ctx.parentNode.clientHeight : 370;
                    function bwabChartAreaGradient(topColor, bottomColor) {
                        var gradient = chartCtx.createLinearGradient(0, 0, 0, chartHeight);
                        gradient.addColorStop(0, topColor);
                        gradient.addColorStop(1, bottomColor);
                        return gradient;
                    }

                    window.myRevenueChart = new Chart(ctx, {
                        type: "line",
                        data: {
                            labels: chartData.labels,
                            datasets: [{
                                label: "' . $received_payments_label . '",
                                data: chartData.received_values,
                                borderColor: "#7c3aed",
                                backgroundColor: bwabChartAreaGradient("rgba(124, 58, 237, 0.38)", "rgba(124, 58, 237, 0.02)"),
                                borderWidth: 2.5,
                                tension: 0.42,
                                fill: true,
                                pointRadius: 0,
                                pointHoverRadius: 5,
                                pointHoverBackgroundColor: "#7c3aed"
                            }, {
                                label: "' . $pending_payments_label . '",
                                data: chartData.pending_values,
                                borderColor: "#ef4444",
                                backgroundColor: bwabChartAreaGradient("rgba(239, 68, 68, 0.28)", "rgba(239, 68, 68, 0.02)"),
                                borderWidth: 2.5,
                                tension: 0.42,
                                fill: true,
                                pointRadius: 0,
                                pointHoverRadius: 5,
                                pointHoverBackgroundColor: "#ef4444"
                            }]
                        },
                        options: {
                            responsive: true,
                            maintainAspectRatio: false,
                            interaction: {
                                mode: "index",
                                intersect: false
                            },
                            plugins: {
                                legend: {
                                    display: true,
                                    position: "top",
                                    align: "end",
                                    labels: {
                                        usePointStyle: true,
                                        pointStyle: "circle",
                                        boxWidth: 8,
                                        boxHeight: 8,
                                        color: "#64748b",
                                        font: {
                                            family: "Inter, sans-serif",
                                            size: 12,
                                            weight: "500"
                                        }
                                    }
                                },
                                tooltip: {
                                    backgroundColor: "#1e293b",
                                    titleFont: { family: "Inter, sans-serif", size: 12 },
                                    bodyFont: { family: "Inter, sans-serif", size: 12 },
                                    padding: 12,
                                    cornerRadius: 8
                                }
                            },
                            scales: {
                                x: {
                                    grid: {
                                        color: "rgba(15, 23, 42, 0.06)",
                                        drawBorder: false
                                    },
                                    ticks: {
                                        color: "#64748b",
                                        maxRotation: 45,
                                        minRotation: 0,
                                        font: {
                                            family: "Inter, sans-serif",
                                            size: 11
                                        }
                                    }
                                },
                                y: {
                                    beginAtZero: true,
                                    grid: {
                                        color: "rgba(15, 23, 42, 0.06)",
                                        drawBorder: false
                                    },
                                    border: {
                                        display: false
                                    },
                                    ticks: {
                                        color: "#64748b",
                                        font: {
                                            family: "Inter, sans-serif",
                                            size: 11
                                        },
                                        callback: function(value) {
                                            var numValue = Number(value);
                                            if (isNaN(numValue)) {
                                                return value;
                                            }
                                            var currencySymbol = "$";
                                            var currencyPos = "before";
                                            if (vuedata.settings_data && vuedata.settings_data.general) {
                                                if (vuedata.settings_data.general.currency_symbol) {
                                                    currencySymbol = vuedata.settings_data.general.currency_symbol;
                                                }
                                                if (vuedata.settings_data.general.currency_position) {
                                                    currencyPos = vuedata.settings_data.general.currency_position;
                                                }
                                            }
                                            return currencyPos === "after"
                                                ? numValue + currencySymbol
                                                : currencySymbol + numValue;
                                        }
                                    }
                                }
                            }
                        }
                    });
                },
            ';

            return $bookwellab_page_method_data;
        }

        /**
         * @return array{filter:string,custom_date:array,start_date:string,end_date:string,is_all_time_filter:bool,timezone:\DateTimeZone}
         */
        private function get_dashboard_request_context()
        {
            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')]);
            }

            // BookWellAB_Input: unslashed string; sanitized on next line before use.
            $filter_raw = BookWellAB_Input::get_post_string('filter');
            $filter = $filter_raw !== '' ? sanitize_text_field($filter_raw) : 'this_month';
            // BookWellAB_Input: scalars sanitized via map_deep; used in date-range resolution below.
            $custom_date = BookWellAB_Input::get_post_array('custom_date');
            $range = $this->resolve_dashboard_date_range($filter, $custom_date);

            return array_merge(
                [
                    'filter' => $filter,
                    'custom_date' => $custom_date,
                ],
                $range
            );
        }

        /**
         * @param string $filter Filter key.
         * @param array  $custom_date Custom range from UI.
         * @return array{start_date:string,end_date:string,is_all_time_filter:bool,timezone:\DateTimeZone}
         */
        private function resolve_dashboard_date_range($filter, $custom_date)
        {
            global $wpdb;

            $timezone = wp_timezone();
            $now = new \DateTimeImmutable('now', $timezone);
            $start_date = '';
            $end_date = '';
            $is_all_time_filter = false;

            switch ($filter) {
                case 'this_month':
                    $start_date = $now->modify('first day of this month')->format('Y-m-d');
                    $end_date = $now->modify('last day of this month')->format('Y-m-d');
                    break;
                case 'last_3_months':
                    $start_date = $now->modify('first day of -2 months')->format('Y-m-d');
                    $end_date = $now->modify('last day of this month')->format('Y-m-d');
                    break;
                case 'last_6_months':
                    $start_date = $now->modify('first day of -5 months')->format('Y-m-d');
                    $end_date = $now->modify('last day of this month')->format('Y-m-d');
                    break;
                case 'all_time':
                    $is_all_time_filter = true;
                    $appt_table = esc_sql($this->bwdb->get_table_name('appointments'));
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped.
                    $min_max = $wpdb->get_row("SELECT MIN(bw_date) as min_date, MAX(bw_date) as max_date FROM $appt_table");
                    if (!empty($min_max->min_date) && !empty($min_max->max_date)) {
                        $start_date = sanitize_text_field((string) $min_max->min_date);
                        $end_date = sanitize_text_field((string) $min_max->max_date);
                    } else {
                        $start_date = $now->modify('first day of this month')->format('Y-m-d');
                        $end_date = $now->modify('last day of this month')->format('Y-m-d');
                    }
                    break;
                case 'custom':
                    if (is_array($custom_date) && count($custom_date) === 2) {
                        $custom_start = sanitize_text_field((string) $custom_date[0]);
                        $custom_end = sanitize_text_field((string) $custom_date[1]);
                        $start_timestamp = strtotime($custom_start);
                        $end_timestamp = strtotime($custom_end);
                        if ($start_timestamp && $end_timestamp) {
                            $start_date = wp_date('Y-m-d', $start_timestamp, $timezone);
                            $end_date = wp_date('Y-m-d', $end_timestamp, $timezone);
                        } else {
                            $start_date = $now->modify('first day of this month')->format('Y-m-d');
                            $end_date = $now->modify('last day of this month')->format('Y-m-d');
                        }
                    } else {
                        $start_date = $now->modify('first day of this month')->format('Y-m-d');
                        $end_date = $now->modify('last day of this month')->format('Y-m-d');
                    }
                    break;
                default:
                    $start_date = $now->modify('first day of this month')->format('Y-m-d');
                    $end_date = $now->modify('last day of this month')->format('Y-m-d');
                    break;
            }

            return [
                'start_date' => $start_date,
                'end_date' => $end_date,
                'is_all_time_filter' => $is_all_time_filter,
                'timezone' => $timezone,
            ];
        }

        public function bookwellab_get_dashboard_counts_func()
        {
            global $wpdb;

            $ctx = $this->get_dashboard_request_context();
            $start_date = $ctx['start_date'];
            $end_date = $ctx['end_date'];
            $timezone = $ctx['timezone'];

            $appt_table = esc_sql($this->bwdb->get_table_name('appointments'));
            $cust_table = esc_sql($this->bwdb->get_table_name('customers'));
            $customer_range_start = $start_date . ' 00:00:00';
            $customer_range_end = (new \DateTimeImmutable($end_date, $timezone))->modify('+1 day')->format('Y-m-d H:i:s');

            // 1. Counts — single appointments scan (was 7 separate COUNT queries).
            $stats_sql = "SELECT
                COUNT(*) AS total_appointments,
                SUM(CASE WHEN bw_status = 'pending' THEN 1 ELSE 0 END) AS pending_appointments,
                SUM(CASE WHEN bw_status = 'approved' THEN 1 ELSE 0 END) AS approved_appointments,
                SUM(CASE WHEN bw_status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled_appointments,
                COUNT(DISTINCT bw_customer_id) AS total_customers,
                SUM(CASE WHEN bw_payment_status = 'received' THEN 1 ELSE 0 END) AS received_payments,
                SUM(CASE WHEN bw_payment_status = 'pending' THEN 1 ELSE 0 END) AS pending_payments
                FROM $appt_table
                WHERE bw_date >= %s AND bw_date <= %s";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and values are prepared.
            $stats_row = $wpdb->get_row($wpdb->prepare($stats_sql, $start_date, $end_date));

            $total_Appts = $stats_row ? (int) $stats_row->total_appointments : 0;
            $pending_Appts = $stats_row ? (int) $stats_row->pending_appointments : 0;
            $approved_Appts = $stats_row ? (int) $stats_row->approved_appointments : 0;
            $cancelled_Appts = $stats_row ? (int) $stats_row->cancelled_appointments : 0;
            $total_customers = $stats_row ? (int) $stats_row->total_customers : 0;
            $received_payments = $stats_row ? (int) $stats_row->received_payments : 0;
            $pending_payments = $stats_row ? (int) $stats_row->pending_payments : 0;

            // New customers (index-friendly range on bw_create_at, not DATE()).
            $new_customers_sql = "SELECT COUNT(*) FROM $cust_table WHERE bw_create_at >= %s AND bw_create_at < %s";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled; query uses prepare().
            $new_customers = $wpdb->get_var($wpdb->prepare($new_customers_sql, $customer_range_start, $customer_range_end));

            wp_send_json_success([
                'total_appointments' => $total_Appts,
                'pending_appointments' => $pending_Appts,
                'approved_appointments' => $approved_Appts,
                'cancelled_appointments' => $cancelled_Appts,
                'new_customers' => (int) $new_customers,
                'total_customers' => $total_customers,
                'received_payments' => $received_payments,
                'pending_payments' => $pending_payments,
            ]);
        }

        public function bookwellab_get_dashboard_chart_func()
        {
            global $wpdb;

            $ctx = $this->get_dashboard_request_context();
            $start_date = $ctx['start_date'];
            $end_date = $ctx['end_date'];
            $is_all_time_filter = $ctx['is_all_time_filter'];
            $timezone = $ctx['timezone'];

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

            $start_dt = new \DateTime($start_date);
            $end_dt = new \DateTime($end_date);
            $diff_days = $start_dt->diff($end_dt)->days;

            $settings = \BookWellAB\BookWellAB_Settings::get_settings();
            $date_format = isset($settings['general']['date_format']) ? $settings['general']['date_format'] : 'Y-m-d';

            if ($is_all_time_filter) {
                // "All Time" should always render as yearly view for the latest 5 years.
                $group_sql = "DATE_FORMAT(bw_date, '%Y')";
                $php_format = 'Y';
                $interval = 'P1Y';
                $label_format = 'Y';

                // Anchor to latest appointment year so historical datasets still render data.
                $anchor = new \DateTimeImmutable($end_date, $timezone);
                $chart_start = $anchor->modify('first day of january this year')->modify('-4 years');
                $chart_end = $anchor->modify('last day of december this year');
            } elseif ($diff_days > 365) {
                // Yearly aggregation
                $group_sql = "DATE_FORMAT(bw_date, '%Y')";
                $php_format = 'Y';
                $interval = 'P1Y';
                $label_format = 'Y';
                $chart_start = (new \DateTime($start_date))->modify('first day of January this year');
                $chart_end = (new \DateTime($end_date))->modify('last day of December this year');
            } elseif ($diff_days > 60) {
                // Monthly aggregation
                $group_sql = "DATE_FORMAT(bw_date, '%Y-%m')";
                $php_format = 'Y-m';
                $interval = 'P1M';
                $label_format = 'M Y';
                $chart_start = (new \DateTime($start_date))->modify('first day of this month');
                $chart_end = (new \DateTime($end_date))->modify('last day of this month');
            } else {
                // Daily aggregation
                $group_sql = "bw_date";
                $php_format = 'Y-m-d';
                $interval = 'P1D';
                $label_format = $date_format;
                $chart_start = new \DateTime($start_date);
                $chart_end = new \DateTime($end_date);
            }

            $revenue_sql = "SELECT $group_sql as period, 
                            SUM(CASE WHEN bw_payment_status = 'received' THEN bw_price ELSE 0 END) as received_revenue,
                            SUM(CASE WHEN bw_payment_status = 'pending' THEN bw_price ELSE 0 END) as pending_revenue
                            FROM $appt_table 
                            WHERE bw_date >= %s AND bw_date <= %s 
                            AND (bw_status = 'approved' OR bw_status = 'pending')
                            GROUP BY period 
                            ORDER BY period ASC";

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Group/table SQL fragments are controlled and date values are prepared.
            $revenue_data = $wpdb->get_results($wpdb->prepare($revenue_sql, $start_date, $end_date));

            $periodic_received = [];
            $periodic_pending = [];
            foreach ($revenue_data as $row) {
                $periodic_received[$row->period] = (float) $row->received_revenue;
                $periodic_pending[$row->period] = (float) $row->pending_revenue;
            }

            // Generate full period to fill gaps
            $period = new \DatePeriod(
                $chart_start,
                new \DateInterval($interval),
                $chart_end->modify('+1 day')
            );

            $chart_labels = [];
            $received_values = [];
            $pending_values = [];

            foreach ($period as $value) {
                $p = $value->format($php_format);
                $chart_labels[] = $value->format($label_format);
                $received_values[] = isset($periodic_received[$p]) ? $periodic_received[$p] : 0;
                $pending_values[] = isset($periodic_pending[$p]) ? $periodic_pending[$p] : 0;
            }

            wp_send_json_success([
                'revenue_chart' => [
                    'labels' => $chart_labels,
                    'received_values' => $received_values,
                    'pending_values' => $pending_values,
                ],
            ]);
        }

        public function bookwellab_get_dashboard_top_services_func()
        {
            global $wpdb;

            $ctx = $this->get_dashboard_request_context();
            $start_date = $ctx['start_date'];
            $end_date = $ctx['end_date'];

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

            $top_services_sql = "SELECT bw_service_id, bw_service_name, COUNT(*) as count 
                                 FROM $appt_table 
                                 WHERE bw_date >= %s AND bw_date <= %s
                                 GROUP BY bw_service_id, bw_service_name
                                 ORDER BY count DESC 
                                 LIMIT 5";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and values are prepared.
            $top_services = $wpdb->get_results($wpdb->prepare($top_services_sql, $start_date, $end_date));

            wp_send_json_success([
                'top_services' => $top_services,
            ]);
        }

        /**
         * Staff and service options for day preview filters.
         *
         * @return array{staff_list: array, service_list: array, default_staff_id: int}
         */
        private function get_day_preview_filter_options()
        {
            global $wpdb;

            $staff_table = esc_sql($this->bwdb->get_table_name('staffs'));
            $services_table = esc_sql($this->bwdb->get_table_name('services'));

            $staff_sql = "SELECT bw_staff_id, bw_staff_name, bw_staff_image FROM $staff_table WHERE bw_staff_is_active = %d ORDER BY bw_staff_position_order ASC, bw_staff_id ASC";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; scalar filter is prepared.
            $staff_rows = $wpdb->get_results($wpdb->prepare($staff_sql, 1), ARRAY_A);

            $services_sql = "SELECT bw_service_id, bw_service_name FROM $services_table WHERE bw_service_status = %d ORDER BY bw_service_position ASC, bw_service_id ASC";
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled and escaped; scalar filter is prepared.
            $service_rows = $wpdb->get_results($wpdb->prepare($services_sql, 1), ARRAY_A);

            $staff_list = [];
            $default_staff_id = 0;
            foreach ((array) $staff_rows as $index => $row) {
                $staff_id = (int) ($row['bw_staff_id'] ?? 0);
                if ($staff_id <= 0) {
                    continue;
                }
                if ($index === 0) {
                    $default_staff_id = $staff_id;
                }
                $image = !empty($row['bw_staff_image']) ? $row['bw_staff_image'] : BookWellAB_Frontend::get_default_staff_image_url();
                $staff_list[] = [
                    'id' => $staff_id,
                    'name' => (string) ($row['bw_staff_name'] ?? ''),
                    'image' => esc_url($image),
                ];
            }

            $service_list = [];
            foreach ((array) $service_rows as $row) {
                $service_id = (int) ($row['bw_service_id'] ?? 0);
                if ($service_id <= 0) {
                    continue;
                }
                $service_list[] = [
                    'id' => $service_id,
                    'name' => (string) ($row['bw_service_name'] ?? ''),
                ];
            }

            return [
                'staff_list' => $staff_list,
                'service_list' => $service_list,
                'default_staff_id' => $default_staff_id,
            ];
        }

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

            // BookWellAB_Input: unslashed strings; each field sanitized or cast below before query.
            $date_raw = BookWellAB_Input::get_post_string('date');
            $date = $date_raw !== '' ? sanitize_text_field($date_raw) : wp_date('Y-m-d', null, wp_timezone());
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
                $date = wp_date('Y-m-d', null, wp_timezone());
            }

            $staff_id = absint(BookWellAB_Input::get_post_string('staff_id'));
            $service_raw = BookWellAB_Input::get_post_string('service_id');
            $service_id = ($service_raw === 'all' || $service_raw === '') ? 'all' : absint($service_raw);
            $view = BookWellAB_Input::get_post_string('view');
            $view = in_array($view, ['appointments', 'availability'], true) ? $view : 'appointments';

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

            $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'));
            $status_clause = " AND (bw_status IS NULL OR LOWER(TRIM(bw_status)) NOT IN ('cancelled', 'canceled')) ORDER BY bw_time ASC";
            if ($service_id !== 'all') {
                $appointments_sql = "SELECT * FROM $appt_table WHERE bw_date = %s AND bw_staff_id = %d AND bw_service_id = %d" . $status_clause;
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is plugin-controlled; values use prepare().
                $appointment_rows = $wpdb->get_results($wpdb->prepare($appointments_sql, $date, $staff_id, $service_id), ARRAY_A);
            } else {
                $appointments_sql = "SELECT * FROM $appt_table WHERE bw_date = %s AND bw_staff_id = %d" . $status_clause;
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is 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);
            $availability = $this->build_day_preview_availability($schedule, $appointments, $staff_id, $date, $service_id, $timeline);

            $timezone = wp_timezone();
            $now_percent = null;
            $now_label = '';
            $today = wp_date('Y-m-d', null, $timezone);
            if ($date === $today && $timeline) {
                $now_dt = new \DateTimeImmutable('now', $timezone);
                $now_minutes = ((int) $now_dt->format('G')) * 60 + (int) $now_dt->format('i');
                $range = $timeline['end_minutes'] - $timeline['start_minutes'];
                if ($range > 0 && $now_minutes >= $timeline['start_minutes'] && $now_minutes <= $timeline['end_minutes']) {
                    $now_percent = round((($now_minutes - $timeline['start_minutes']) / $range) * 100, 2);
                    $now_label = strtolower($now_dt->format('g:ia'));
                }
            }

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

        /**
         * @param int    $staff_id Staff ID.
         * @param string $date     Y-m-d.
         * @return array{is_off: bool, start_time: string, end_time: string, breaks: array}
         */
        private function get_staff_day_schedule($staff_id, $date)
        {
            return BookWellAB_Booking_Helper::get_staff_day_schedule($staff_id, $date);
        }

        /**
         * @param array  $schedule         Schedule data.
         * @param array  $appointment_rows Raw appointments.
         * @param string $date             Y-m-d.
         * @return array|null
         */
        private function build_day_preview_timeline($schedule, $appointment_rows, $date)
        {
            return BookWellAB_Booking_Helper::build_staff_day_timeline($schedule, $appointment_rows, $date);
        }

        /**
         * @param array      $appointment_rows Rows.
         * @param string     $date             Date.
         * @param array|null $timeline         Timeline.
         * @return array<int, array<string, mixed>>
         */
        private function format_day_preview_appointments($appointment_rows, $date, $timeline)
        {
            return BookWellAB_Booking_Helper::format_staff_day_appointments($appointment_rows, $date, $timeline);
        }

        /**
         * @param array      $schedule     Schedule.
         * @param array      $appointments Formatted appointments.
         * @param int        $staff_id     Staff ID.
         * @param string     $date         Date.
         * @param int|string $service_id   Service filter.
         * @param array|null $timeline     Timeline.
         * @return array<int, array<string, mixed>>
         */
        private function build_day_preview_availability($schedule, $appointments, $staff_id, $date, $service_id, $timeline)
        {
            if (empty($timeline) || !empty($schedule['is_off'])) {
                return [];
            }

            $segments = [];
            $work_start = BookWellAB_Booking_Helper::time_to_minutes($schedule['start_time']);
            $work_end = BookWellAB_Booking_Helper::time_to_minutes($schedule['end_time']);
            if ($work_start <= 0 || $work_end <= 0) {
                $work_start = $timeline['start_minutes'];
                $work_end = $timeline['end_minutes'];
            }

            $segments[] = [
                'type' => 'available',
                'start_minutes' => max($timeline['start_minutes'], $work_start),
                'end_minutes' => min($timeline['end_minutes'], $work_end),
            ];

            foreach ((array) ($schedule['breaks'] ?? []) as $break) {
                if (empty($break['start']) || empty($break['end'])) {
                    continue;
                }
                $segments[] = [
                    'type' => 'break',
                    'start_minutes' => BookWellAB_Booking_Helper::time_to_minutes((string) $break['start']),
                    'end_minutes' => BookWellAB_Booking_Helper::time_to_minutes((string) $break['end']),
                ];
            }

            foreach ($appointments as $appt) {
                $segments[] = [
                    'type' => 'booked',
                    'start_minutes' => (int) $appt['start_minutes'],
                    'end_minutes' => (int) $appt['end_minutes'],
                ];
            }

            if ($service_id !== 'all') {
                $slots = BookWellAB_Booking_Helper::get_staff_available_slots($date, $staff_id, (int) $service_id);
                $slot_segments = [];
                foreach ($slots as $slot) {
                    $slot_start = BookWellAB_Booking_Helper::time_to_minutes((string) $slot);
                    if ($slot_start <= 0) {
                        continue;
                    }
                    $settings = BookWellAB_Settings::get_settings();
                    $slot_step = isset($settings['general']['slot_step']) ? $settings['general']['slot_step'] : '15';
                    $step = ($slot_step === 'service_duration') ? 30 : max(15, (int) $slot_step);
                    $slot_segments[] = [
                        'type' => 'slot',
                        'start_minutes' => $slot_start,
                        'end_minutes' => $slot_start + $step,
                    ];
                }
                if (!empty($slot_segments)) {
                    return $slot_segments;
                }
            }

            return $segments;
        }

    }



}
// phpcs:enable