<?php
namespace Bookaro\Controller\Frontend;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- meta_query is required to resolve bookable listings for asset enqueueing and is bounded by the current page context.

use Bookaro\Frontend\I18n;
use Bookaro\Repository\BookingRepository;
use Bookaro\Service\ScheduleService;
use Bookaro\Controller\Frontend\ShortcodesController;
use Bookaro\Utils\BookingPolicyPresenter;
use function bookaro_get_user_meta;

/**
 * Enqueues frontend scripts and styles and outputs inline custom styles.
 */
class AssetsController
{
    private static ?self $instance = null;
    private static bool $late_styles_hooked = false;
    private static bool $fallback_styles_hooked = false;
    private BookingRepository $db;
    private bool $needs_frontend_styles = false;

    public function __construct( BookingRepository $db )
    {
        self::$instance = $this;
        $this->db = $db;
    }

    public static function instance(): ?self
    {
        return self::$instance;
    }

    public function ensure_frontend_assets(): void
    {
        if ( ! did_action( 'wp_enqueue_scripts' ) ) {
            if ( ! has_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ) ) {
                add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 20 );
            }
            return;
        }

        $this->enqueue_assets();

        if ( did_action( 'wp_head' ) && ! self::$late_styles_hooked ) {
            add_action( 'wp_footer', 'wp_print_styles', 5 );
            self::$late_styles_hooked = true;
        }
        if ( did_action( 'wp_head' ) && ! self::$fallback_styles_hooked ) {
            add_action( 'wp_footer', [ $this, 'print_fallback_styles' ], 6 );
            self::$fallback_styles_hooked = true;
        }
    }

    public function print_fallback_styles(): void
    {
        $styles = [
            'bookaro-booking-tailwind' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/vendor/tailwind/tailwind.min.css',
                'ver' => '2.2.19',
            ],
            'bookaro-booking-style' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/css/booking-base.css',
                'ver' => BOOKARO_VERSION,
            ],
            'bookaro-booking-calendar' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/css/calendar-picker.css',
                'ver' => BOOKARO_VERSION,
            ],
            'bookaro-provider-dashboard-style' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/css/provider-dashboard-base.css',
                'ver' => BOOKARO_VERSION,
            ],
            'bookaro-owner-dashboard-style' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/css/owner-dashboard-base.css',
                'ver' => BOOKARO_VERSION,
            ],
            'bookaro-dashboard-ui' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/css/dashboard-ui.css',
                'ver' => BOOKARO_VERSION,
            ],
            'flatpickr-css' => [
                'src' => BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.css',
                'ver' => '4.6.13',
            ],
        ];

        foreach ( $styles as $handle => $data ) {
            $src = $data['src'];
            $ver = $data['ver'];
            if ( wp_style_is( $handle, 'done' ) ) {
                continue;
            }
            if ( ! wp_style_is( $handle, 'registered' ) ) {
                wp_register_style( $handle, $src, [], $ver );
            }
            if ( ! wp_style_is( $handle, 'enqueued' ) ) {
                wp_enqueue_style( $handle );
            }
            wp_print_styles( $handle );

            if ( wp_style_is( $handle, 'done' ) ) {
                continue;
            }

            $url = add_query_arg( 'ver', rawurlencode( (string) $ver ), $src );
            $fallback_handle = $handle . '-fallback';
            if ( ! wp_style_is( $fallback_handle, 'registered' ) ) {
                wp_register_style( $fallback_handle, $url, [], $ver );
            }
            if ( ! wp_style_is( $fallback_handle, 'enqueued' ) ) {
                wp_enqueue_style( $fallback_handle );
            }
            wp_print_styles( $fallback_handle );
        }
    }

    /**
     * Register all hooks for the front-end.
     */
    public function register_hooks(): void
    {
        // Appearance CSS is attached to a dedicated registered style handle via
        // wp_add_inline_style() (see print_custom_styles), so it must run during
        // the style enqueue phase rather than printing an inline style element
        // on wp_head.
        add_action( 'wp_enqueue_scripts', [ $this, 'print_custom_styles' ], 21 );
        add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 20 );
    }

    /**
     * Enqueue scripts and styles for both the booking modal and provider dashboard.
     */
    public function enqueue_assets(): void
    {
        if ( function_exists( 'bookaro_is_frontend_enabled' ) && ! bookaro_is_frontend_enabled() ) {
            return;
        }
        // Dashicons are used in the setup notice popup
        wp_enqueue_style( 'dashicons' );

        $integration_mode = function_exists( 'bookaro_get_integration_mode' )
            ? bookaro_get_integration_mode()
            : 'auto';
        $ui_mode          = bookaro_get_ui_mode();
        $require_login    = ! empty( bookaro_get_option( 'require_login', 1 ) );
        $is_pro           = function_exists( 'bookaro_is_pro_active' ) && bookaro_is_pro_active();
        $currency         = function_exists( 'bookaro_get_currency_settings' )
            ? bookaro_get_currency_settings()
            : [];
        $reports_enabled = $is_pro && (bool) apply_filters( 'bookaro_feature_enabled', false, 'reports' );
        $exports_enabled = $is_pro && (bool) apply_filters( 'bookaro_feature_enabled', false, 'exports' );
        $mylisting_enabled = (bool) apply_filters( 'bookaro_feature_enabled', false, 'mylisting' );
        $features = [
            'mylisting' => $mylisting_enabled,
        ];
        $features = apply_filters( 'bookaro_frontend_features', $features, $is_pro );
        $setup_notice_enabled = function_exists( 'bookaro_get_option' )
            ? (bool) bookaro_get_option( 'setup_notice_enabled', 1 )
            : true;
        $setup_notice_flags = [
            'timeslots'     => function_exists( 'bookaro_get_option' ) ? (bool) bookaro_get_option( 'setup_notice_timeslots', 1 ) : true,
            'services'      => function_exists( 'bookaro_get_option' ) ? (bool) bookaro_get_option( 'setup_notice_services', 1 ) : true,
            'exceptions'    => function_exists( 'bookaro_get_option' ) ? (bool) bookaro_get_option( 'setup_notice_exceptions', 1 ) : true,
        ];
        $mylisting_active = ( $mylisting_enabled && $integration_mode === 'mylisting' );
        $skip_booking_assets = $mylisting_active
            && function_exists( 'bookaro_is_listing_singular' )
            && bookaro_is_listing_singular();
        if ( ShortcodesController::needs_booking_assets() && ! $skip_booking_assets ) {
            $user = wp_get_current_user();

            wp_enqueue_script( 'jquery' );
            $this->enqueue_dayjs_locale();
            wp_enqueue_style ( 'flatpickr-css', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.css', [], '4.6.13' );
            wp_enqueue_script( 'flatpickr-js', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.js', [ 'jquery' ], '4.6.13', true );
            wp_enqueue_style ( 'bookaro-booking-tailwind', BOOKARO_PLUGIN_URL . 'assets/vendor/tailwind/tailwind.min.css', [], '2.2.19' );
            wp_enqueue_style ( 'bookaro-booking-style', BOOKARO_PLUGIN_URL . 'assets/css/booking-base.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-booking-calendar', BOOKARO_PLUGIN_URL . 'assets/css/calendar-picker.css', [], BOOKARO_VERSION );
            [ $booking_script_src, $booking_script_ver ] = $this->resolve_pro_dashboard_script(
                'dist/booking.js',
                BOOKARO_PLUGIN_URL . 'dist/booking.js',
                BOOKARO_VERSION
            );
            wp_enqueue_script( 'bookaro-booking-script', $booking_script_src, [ 'jquery', 'flatpickr-js', 'dayjs' ], $booking_script_ver, true );
            $this->needs_frontend_styles = true;

            // Round 8 fix (Issue #23): `get_the_ID()` requires The Loop, but
            // `wp_enqueue_scripts` fires before The Loop runs — on a singular
            // listing visit `get_the_ID()` can return 0. Fall back to
            // `get_queried_object_id()` (populated as soon as the main query
            // resolves) and then to `$post->ID`. Without this, on code paths
            // that don't short-circuit to `bookaro_enqueue_mylisting_assets()`
            // the booking widget receives `listing_id=0` / `provider_id=0` and
            // calls `/provider/0/availability`, so every date renders
            // "unavailable" even though the provider's calendar is open.
            $listing_id = 0;
            if ( function_exists( 'bookaro_is_allowed_listing' ) ) {
                $candidates = [];
                if ( function_exists( 'get_the_ID' ) ) {
                    $candidates[] = (int) get_the_ID();
                }
                if ( function_exists( 'get_queried_object_id' ) ) {
                    $candidates[] = (int) get_queried_object_id();
                }
                global $post;
                if ( $post instanceof \WP_Post ) {
                    $candidates[] = (int) $post->ID;
                }
                foreach ( array_unique( array_filter( $candidates ) ) as $candidate_id ) {
                    if ( $candidate_id > 0 && bookaro_is_allowed_listing( $candidate_id ) ) {
                        $listing_id = $candidate_id;
                        break;
                    }
                }
            }
            $listing_title = $listing_id > 0 ? get_the_title( $listing_id ) : '';
            $listing_url = $listing_id > 0 ? get_permalink( $listing_id ) : '';
            $provider_context_id = 0;
            if ( $listing_id > 0 && function_exists( 'bookaro_get_listing_owner_id' ) ) {
                $provider_context_id = (int) bookaro_get_listing_owner_id( $listing_id );
            }
            // Single-business booking pages have no listing context, so the
            // listing-only resolution above leaves BookaroData.services empty.
            // The booking widget's in-modal service picker only renders when it
            // has services (BookingApp.vue: usesBookaroServices), so with none
            // the customer is never shown a service, picks date/time, fills in
            // their details, and the booking then dead-ends server-side with
            // "Service is required" — the Setup Wizard's own booking-form page
            // (wp:bookaro/booking-form) hits exactly this. Provider resolution
            // and availability already work (the shortcode button carries the
            // resolved provider id); only the service catalog was missing.
            // Surface the configured default provider's services so the picker
            // appears (auto-selecting when there's a single service). Scoped to
            // single-business + no listing — directory / mylisting / per-listing
            // flows are untouched (they populate services via $listing_id).
            $bookaro_sb_services = [];
            $bookaro_sb_services_source = '';
            if ( $listing_id < 1
                && function_exists( 'bookaro_is_single_business_site' )
                && bookaro_is_single_business_site()
                && function_exists( 'bookaro_get_provider_services_for_context' )
            ) {
                $bookaro_sb_provider = $provider_context_id > 0
                    ? $provider_context_id
                    : ( function_exists( 'bookaro_get_option' ) ? (int) bookaro_get_option( 'default_provider_id', 0 ) : 0 );
                if ( $bookaro_sb_provider > 0 ) {
                    $catalog = bookaro_get_provider_services_for_context( $bookaro_sb_provider );
                    if ( is_array( $catalog ) && ! empty( $catalog ) ) {
                        $bookaro_sb_services = array_values( $catalog );
                        $bookaro_sb_services_source = 'bookaro';
                    }
                }
            }
            // Generic Directory lane: when the booking modal is opened from a
            // listing's Book Now button (which carries listing/provider context
            // but no explicit service), surface the provider's bookable services
            // so the customer gets a service picker instead of a dead-end modal
            // with nothing to confirm. Single Business is handled above; the
            // MyListing lane resolves services from the listing repeater via
            // bookaro_enqueue_mylisting_assets() and never reaches this path.
            // Uses the same model-aware resolver the Generic validation relies
            // on, so the modal shows exactly the customer-visible services.
            if ( empty( $bookaro_sb_services )
                && $listing_id > 0
                && $provider_context_id > 0
                && 'mylisting' !== $integration_mode
                && function_exists( 'bookaro_is_single_business_site' )
                && ! bookaro_is_single_business_site()
                && function_exists( 'bookaro_get_generic_provider_context_services' )
            ) {
                $generic_catalog = bookaro_get_generic_provider_context_services( $provider_context_id );
                if ( is_array( $generic_catalog ) && ! empty( $generic_catalog ) ) {
                    $bookaro_sb_services = array_values( $generic_catalog );
                    $bookaro_sb_services_source = 'bookaro';
                }
            }
            $provider_user = $provider_context_id > 0 ? get_user_by( 'ID', $provider_context_id ) : null;
            // Bug 41: gate provider PII behind login. The booking modal only
            // displays email/phone on the post-booking confirmation panel
            // (BookingApp.vue lines 311–316), yet AssetsController was
            // inlining these fields into window.BookaroData on every page
            // that rendered the booking shortcode — including anonymous
            // page loads where the visitor never started a booking.
            // Result: anyone could view source and scrape provider
            // contact info off the public listing pages.
            //
            // Only expose email/phone to logged-in visitors. Display name
            // stays unconditionally (it's intentionally public on the
            // listing). Sites that genuinely want public provider contact
            // info can hook bookaro_booking_frontend_data and add it back.
            $provider_pii_visible = is_user_logged_in();
            $provider_data = [
                'id' => $provider_context_id,
                'name' => $provider_user ? $provider_user->display_name : '',
                'email' => ( $provider_pii_visible && $provider_user ) ? $provider_user->user_email : '',
                'phone' => ( $provider_pii_visible && $provider_context_id > 0 ) ? (string) get_user_meta( $provider_context_id, 'billing_phone', true ) : '',
            ];
            $listing_location = '';
            if ( $listing_id > 0 ) {
                foreach ( [ 'job_location', 'geolocation_formatted_address', 'listing_location', 'location', 'address' ] as $meta_key ) {
                    $value = get_post_meta( $listing_id, $meta_key, true );
                    if ( is_string( $value ) && trim( $value ) !== '' ) {
                        $listing_location = trim( $value );
                        break;
                    }
                }
                $listing_location = (string) apply_filters( 'bookaro_booking_location', $listing_location, $listing_id, 0, null );
            }
            $dashboard_url = '';
            if ( bookaro_user_has_dashboard_access( $user->ID ) ) {
                $dashboard_url = function_exists( 'bookaro_get_dashboard_url' )
                    ? bookaro_get_dashboard_url( [ 'listing_id' => $listing_id ] )
                    : site_url( '/my-account/bookings-and-management/' );
            }

            // Round 8 fix (Issue #23): the frontend booking widget reads
            // `BookaroData.provider_id` to call `/provider/{id}/availability`.
            // Previously this controller hardcoded `provider_id => 0` even
            // though it had just resolved `$provider_context_id` from the
            // current listing — so the widget would call
            // `/provider/0/availability` and every date rendered as
            // "unavailable" (no real provider 0 has availability). Surface
            // the resolved provider so the widget can fetch availability
            // correctly on shortcode pages and on any other code path that
            // doesn't short-circuit to bookaro_enqueue_mylisting_assets().
            $context = [
                'listing_id' => $listing_id,
                'provider_id' => $provider_context_id,
                'service_id' => 0,
                'service_label' => '',
            ];
            $booking_data = [
                    'ajax_url'     => admin_url( 'admin-ajax.php' ),
                    'ajax_nonce'   => wp_create_nonce( 'bookaro_nonce' ),
                    'rest_url'     => esc_url_raw( rest_url() ),
                    'rest_nonce'   => wp_create_nonce( 'wp_rest' ),
                    'listing_id'   => $listing_id,
                    'provider_id'  => $provider_context_id,
                    'current_user_id' => get_current_user_id(),
                    'is_logged_in' => is_user_logged_in(),
                    'login_url'    => function_exists( 'bookaro_get_login_url' )
                        ? bookaro_get_login_url( get_permalink() )
                        : wp_login_url( get_permalink() ),
                    'user'         => is_user_logged_in()
                        ? [
                            'name'  => trim( $user->first_name . ' ' . $user->last_name ) ?: $user->display_name,
                            'email' => $user->user_email,
                            'phone' => get_user_meta( $user->ID, 'billing_phone', true ),
                        ]
                        : null,
                    'listing_title' => $listing_title,
                    'listing_price' => 0,
                    'listing_price_formatted' => function_exists( 'bookaro_format_price' )
                        ? bookaro_format_price( 0 )
                        : '',
                    'listing_url'   => $listing_url,
                    'listing_location' => $listing_location,
                    'provider'      => $provider_data,
                    'service_label' => '',
                    'services'      => $bookaro_sb_services,
                    'services_source' => $bookaro_sb_services_source,
                    'services_endpoint' => '',
                    'dashboard_url' => $dashboard_url,
                    'has_timeslots' => true,
                    'policies'   => $this->get_policy_frontend_data(),
                    'currency'    => $currency,
                    'phone_required' => (bool) bookaro_get_option( 'booking_phone_required', 1 ),
                    // Bug 54: surface the require_login setting to the Vue
                    // app so it can match the server's permission gate.
                    // Without this BookingApp.vue routes step=0 (login gate)
                    // for any anonymous visitor regardless of the setting.
                    'require_login' => $require_login,
                    'is_pro'      => $is_pro,
                    'features'    => $features,
                    'i18n'        => I18n::all(),
            ];
            $booking_data = apply_filters( 'bookaro_booking_frontend_data', $booking_data, $context );
            $booking_data = apply_filters( 'bookaro_booking_data', $booking_data, $context, $integration_mode );

            wp_localize_script(
                'bookaro-booking-script',
                'BookaroData',
                $booking_data
            );
        }

        if (
            'shortcodes' === $ui_mode &&
            ShortcodesController::needs_provider_dashboard_assets() &&
            is_user_logged_in() &&
            bookaro_user_has_provider_access()
        ) {
            $opts = get_option( 'bookaro_settings', [] );
            $dashboard_density = sanitize_key( $opts['appearance_dashboard_density'] ?? 'comfortable' );
            if ( ! in_array( $dashboard_density, [ 'comfortable', 'compact' ], true ) ) {
                $dashboard_density = 'comfortable';
            }
            $user = wp_get_current_user();

            wp_enqueue_script( 'jquery' );
            $provider_deps = [ 'jquery' ];
            $this->enqueue_dayjs_locale();
            wp_enqueue_style ( 'flatpickr-css', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.css', [], '4.6.13' );
            wp_enqueue_script( 'flatpickr-js', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.js', [ 'jquery' ], '4.6.13', true );
            wp_enqueue_style ( 'bookaro-booking-tailwind', BOOKARO_PLUGIN_URL . 'assets/vendor/tailwind/tailwind.min.css', [], '2.2.19' );
            wp_enqueue_style ( 'bookaro-booking-style', BOOKARO_PLUGIN_URL . 'assets/css/booking-base.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-provider-dashboard-style', BOOKARO_PLUGIN_URL . 'assets/css/provider-dashboard-base.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-dashboard-ui', BOOKARO_PLUGIN_URL . 'assets/css/dashboard-ui.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-booking-calendar', BOOKARO_PLUGIN_URL . 'assets/css/calendar-picker.css', [], BOOKARO_VERSION );
            $provider_deps[] = 'dayjs';
            $provider_deps[] = 'flatpickr-js';
            if ( $reports_enabled ) {
                $this->maybe_enqueue_reports_assets( $provider_deps );
            }
            [ $provider_script_src, $provider_script_ver ] = $this->resolve_pro_dashboard_script(
                'dist/provider-dashboard.js',
                BOOKARO_PLUGIN_URL . 'dist/provider-dashboard.js',
                BOOKARO_VERSION
            );
            wp_enqueue_script( 'bookaro-provider-dashboard', $provider_script_src, $provider_deps, $provider_script_ver, true );
            $this->needs_frontend_styles = true;

            $schedule  = new ScheduleService( $this->db );
            $timeslots = $schedule->get_timeslots( $user->ID );
            $interval  = ! empty( $timeslots ) ? (int) ( $timeslots[0]->interval_m ?? 30 ) : 30;
            $is_whole_day = ( $interval === 1440 );
            $setup = $this->get_provider_setup_data( $user, $integration_mode, $timeslots );
            $listing_ids = $setup['listing_ids'] ?? [];

            $provider_data = array_merge(
                [
                'ajax_url'   => admin_url( 'admin-ajax.php' ),
                'ajax_nonce' => wp_create_nonce( 'bookaro_nonce' ),
                'user_id'    => get_current_user_id(),
                'is_whole_day' => $is_whole_day,
                'listing_url'  => $setup['listing_url'] ?? '',
                'setup'        => $setup,
                'setup_notice_enabled' => $setup_notice_enabled,
                'setup_notice_flags'   => $setup_notice_flags,

                'rest_url'   => esc_url_raw( rest_url() ),
                'rest_nonce' => wp_create_nonce( 'wp_rest' ),

                'currency_symbol'  => $currency['symbol'] ?? '$',
                'currency'         => $currency,
                'policies'         => $this->get_policy_frontend_data(),
                'exceptions_viewed' => (bool) bookaro_get_user_meta( $user->ID, 'bookaro_viewed_exceptions', false ),
                'integration_mode' => function_exists( 'bookaro_get_integration_mode' )
                    ? bookaro_get_integration_mode()
                    : 'auto',
                'site_type' => function_exists( 'bookaro_get_site_type' )
                    ? sanitize_key( (string) bookaro_get_site_type() )
                    : 'directory',
                'lite_listing_id' => function_exists( 'bookaro_get_lite_listing_id' )
                    ? bookaro_get_lite_listing_id()
                    : 0,
                'provider_services_allowed' => function_exists( 'bookaro_provider_services_allowed' )
                    ? bookaro_provider_services_allowed()
                    : false,
                'is_pro'      => $is_pro,
                'dashboard_density' => $dashboard_density,
                'features' => $features,
                'auto_approve_bookings' => (bool) bookaro_get_option( 'auto_approve_bookings', 0 ),
                'i18n'        => I18n::all(),
                ],
                ( $reports_enabled || $exports_enabled ) ? $this->get_reports_frontend_data() : []
            );
            $provider_data = apply_filters(
                'bookaro_provider_dashboard_data',
                $provider_data,
                [
                    'provider_id' => (int) $user->ID,
                    'listing_ids' => $listing_ids,
                    'integration_mode' => $integration_mode,
                ]
            );

            wp_localize_script(
                'bookaro-provider-dashboard',
                'BookaroData',
                $provider_data
            );
        }

        if (
            'shortcodes' === $ui_mode &&
            ShortcodesController::needs_customer_dashboard_assets() &&
            is_user_logged_in() &&
            bookaro_user_has_customer_access()
        ) {
            $user = wp_get_current_user();
            $setup = $this->get_owner_setup_data( $user );
            $opts = get_option( 'bookaro_settings', [] );
            $dashboard_density = sanitize_key( $opts['appearance_dashboard_density'] ?? 'comfortable' );
            if ( ! in_array( $dashboard_density, [ 'comfortable', 'compact' ], true ) ) {
                $dashboard_density = 'comfortable';
            }
            wp_enqueue_script( 'jquery' );
            $this->enqueue_dayjs_locale();
            wp_enqueue_style ( 'flatpickr-css', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.css', [], '4.6.13' );
            wp_enqueue_script( 'flatpickr-js', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.js', [ 'jquery' ], '4.6.13', true );
            wp_enqueue_style ( 'bookaro-booking-tailwind', BOOKARO_PLUGIN_URL . 'assets/vendor/tailwind/tailwind.min.css', [], '2.2.19' );
            wp_enqueue_style ( 'bookaro-booking-style', BOOKARO_PLUGIN_URL . 'assets/css/booking-base.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-owner-dashboard-style', BOOKARO_PLUGIN_URL . 'assets/css/owner-dashboard-base.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-dashboard-ui', BOOKARO_PLUGIN_URL . 'assets/css/dashboard-ui.css', [], BOOKARO_VERSION );
            wp_enqueue_style ( 'bookaro-booking-calendar', BOOKARO_PLUGIN_URL . 'assets/css/calendar-picker.css', [], BOOKARO_VERSION );
            [ $owner_script_src, $owner_script_ver ] = $this->resolve_pro_dashboard_script(
                'dist/owner-dashboard.js',
                BOOKARO_PLUGIN_URL . 'dist/owner-dashboard.js',
                BOOKARO_VERSION
            );
            wp_enqueue_script( 'bookaro-owner-dashboard', $owner_script_src, [ 'jquery', 'dayjs', 'flatpickr-js' ], $owner_script_ver, true );
            $this->needs_frontend_styles = true;

            $owner_data = [
                'ajax_url'   => admin_url( 'admin-ajax.php' ),
                'ajax_nonce' => wp_create_nonce( 'bookaro_nonce' ),
                'rest_url'   => esc_url_raw( rest_url() ),
                'rest_nonce' => wp_create_nonce( 'wp_rest' ),
                'user_id'    => get_current_user_id(),
                'currency'   => $currency,
                'policies'   => $this->get_policy_frontend_data(),
                'is_pro'      => $is_pro,
                'dashboard_density' => $dashboard_density,
                'features'   => $features,
                'setup'      => $setup,
                'setup_notice_enabled' => $setup_notice_enabled,
                'setup_notice_flags'   => $setup_notice_flags,
                'i18n'       => I18n::all(),
            ];
            $owner_data = apply_filters(
                'bookaro_owner_dashboard_data',
                $owner_data,
                [ 'owner_id' => (int) $user->ID ]
            );
            wp_localize_script(
                'bookaro-owner-dashboard',
                'BookaroData',
                $owner_data
            );
        }

        if (
            'woocommerce' === $ui_mode &&
            function_exists( 'is_wc_endpoint_url' ) &&
            get_query_var( 'bookings-and-management', false ) !== false &&
            ( ! $require_login || is_user_logged_in() )
        ) {
            $opts = get_option( 'bookaro_settings', [] );
            $dashboard_density = sanitize_key( $opts['appearance_dashboard_density'] ?? 'comfortable' );
            if ( ! in_array( $dashboard_density, [ 'comfortable', 'compact' ], true ) ) {
                $dashboard_density = 'comfortable';
            }

            $user = wp_get_current_user();

            wp_enqueue_script( 'jquery' );
            $provider_deps = [ 'jquery' ];
            $this->enqueue_dayjs_locale();
            wp_enqueue_style ( 'flatpickr-css', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.css', [], '4.6.13' );
            wp_enqueue_script( 'flatpickr-js', BOOKARO_PLUGIN_URL . 'assets/vendor/flatpickr/flatpickr.min.js', [ 'jquery' ], '4.6.13', true );
            wp_enqueue_style ( 'bookaro-booking-tailwind', BOOKARO_PLUGIN_URL . 'assets/vendor/tailwind/tailwind.min.css', [], '2.2.19' );
            $this->needs_frontend_styles = true;

            if ( bookaro_user_has_provider_access( $user->ID ) ) {
                wp_enqueue_style ( 'bookaro-booking-style', BOOKARO_PLUGIN_URL . 'assets/css/booking-base.css', [], BOOKARO_VERSION );
                wp_enqueue_style ( 'bookaro-provider-dashboard-style', BOOKARO_PLUGIN_URL . 'assets/css/provider-dashboard-base.css', [], BOOKARO_VERSION );
                wp_enqueue_style ( 'bookaro-dashboard-ui', BOOKARO_PLUGIN_URL . 'assets/css/dashboard-ui.css', [], BOOKARO_VERSION );
                wp_enqueue_style ( 'bookaro-booking-calendar', BOOKARO_PLUGIN_URL . 'assets/css/calendar-picker.css', [], BOOKARO_VERSION );
                $provider_deps[] = 'dayjs';
                $provider_deps[] = 'flatpickr-js';
                if ( $reports_enabled ) {
                    $this->maybe_enqueue_reports_assets( $provider_deps );
                }
                [ $provider_script_src, $provider_script_ver ] = $this->resolve_pro_dashboard_script(
                    'dist/provider-dashboard.js',
                    BOOKARO_PLUGIN_URL . 'dist/provider-dashboard.js',
                    BOOKARO_VERSION
                );
                wp_enqueue_script( 'bookaro-provider-dashboard', $provider_script_src, $provider_deps, $provider_script_ver, true );

                $listing_id = 0;

                $schedule  = new ScheduleService( $this->db );
                $timeslots = $schedule->get_timeslots( $user->ID );
                $interval  = ! empty( $timeslots ) ? (int) ( $timeslots[0]->interval_m ?? 30 ) : 30;
                $is_whole_day = ( $interval === 1440 );
                $setup = $this->get_provider_setup_data( $user, $integration_mode, $timeslots );
                $listing_ids = $setup['listing_ids'] ?? [];

                $provider_data = array_merge(
                    [
                    'ajax_url'   => admin_url( 'admin-ajax.php' ),
                    'ajax_nonce' => wp_create_nonce( 'bookaro_nonce' ),
                    'user_id'    => get_current_user_id(),
                    'is_whole_day' => $is_whole_day,
                    'listing_url'  => $setup['listing_url'] ?? '',
                    'setup'        => $setup,
                    'setup_notice_enabled' => $setup_notice_enabled,
                    'setup_notice_flags'   => $setup_notice_flags,

                'rest_url'   => esc_url_raw( rest_url() ),
                'rest_nonce' => wp_create_nonce( 'wp_rest' ),

                'currency_symbol'  => $currency['symbol'] ?? '$',
                'currency'         => $currency,
                'policies'         => $this->get_policy_frontend_data(),
                'exceptions_viewed' => (bool) bookaro_get_user_meta( $user->ID, 'bookaro_viewed_exceptions', false ),
                'integration_mode' => function_exists( 'bookaro_get_integration_mode' )
                    ? bookaro_get_integration_mode()
                    : 'auto',
                'lite_listing_id' => function_exists( 'bookaro_get_lite_listing_id' )
                    ? bookaro_get_lite_listing_id()
                    : 0,
                'provider_services_allowed' => function_exists( 'bookaro_provider_services_allowed' )
                    ? bookaro_provider_services_allowed()
                    : false,
                'is_pro'      => $is_pro,
                'dashboard_density' => $dashboard_density,
                'features' => $features,
                'auto_approve_bookings' => (bool) bookaro_get_option( 'auto_approve_bookings', 0 ),
                'i18n'        => I18n::all(),
                    ],
                    $reports_enabled ? $this->get_reports_frontend_data() : []
                );
                $provider_data = apply_filters(
                    'bookaro_provider_dashboard_data',
                    $provider_data,
                    [
                        'provider_id' => (int) $user->ID,
                        'listing_ids' => $listing_ids,
                        'integration_mode' => $integration_mode,
                    ]
                );

                wp_localize_script(
                    'bookaro-provider-dashboard',
                    'BookaroData',
                    $provider_data
                );
            } elseif ( bookaro_user_has_customer_access( $user->ID ) ) {
                wp_enqueue_style ( 'bookaro-booking-style', BOOKARO_PLUGIN_URL . 'assets/css/booking-base.css', [], BOOKARO_VERSION );
                wp_enqueue_style ( 'bookaro-owner-dashboard-style', BOOKARO_PLUGIN_URL . 'assets/css/owner-dashboard-base.css', [], BOOKARO_VERSION );
                wp_enqueue_style ( 'bookaro-dashboard-ui', BOOKARO_PLUGIN_URL . 'assets/css/dashboard-ui.css', [], BOOKARO_VERSION );
                wp_enqueue_style ( 'bookaro-booking-calendar', BOOKARO_PLUGIN_URL . 'assets/css/calendar-picker.css', [], BOOKARO_VERSION );
                [ $owner_script_src, $owner_script_ver ] = $this->resolve_pro_dashboard_script(
                    'dist/owner-dashboard.js',
                    BOOKARO_PLUGIN_URL . 'dist/owner-dashboard.js',
                    BOOKARO_VERSION
                );
                wp_enqueue_script( 'bookaro-owner-dashboard', $owner_script_src, [ 'jquery', 'dayjs', 'flatpickr-js' ], $owner_script_ver, true );
                $setup = $this->get_owner_setup_data( $user );

                $owner_data = [
                    'ajax_url'   => admin_url( 'admin-ajax.php' ),
                    'ajax_nonce' => wp_create_nonce( 'bookaro_nonce' ),
                    'rest_url'   => esc_url_raw( rest_url() ),
                    'rest_nonce' => wp_create_nonce( 'wp_rest' ),
                    'user_id'    => get_current_user_id(),
                    'currency'   => $currency,
                'policies'   => $this->get_policy_frontend_data(),
                'is_pro'      => $is_pro,
                'dashboard_density' => $dashboard_density,
                'features'   => $features,
                'setup'      => $setup,
                'setup_notice_enabled' => $setup_notice_enabled,
                'setup_notice_flags'   => $setup_notice_flags,
                'i18n'       => I18n::all(),
                ];
                $owner_data = apply_filters(
                    'bookaro_owner_dashboard_data',
                    $owner_data,
                    [ 'owner_id' => (int) $user->ID ]
                );
                wp_localize_script(
                    'bookaro-owner-dashboard',
                    'BookaroData',
                    $owner_data
                );
        }
        }

        if ( $this->needs_frontend_styles && ! self::$fallback_styles_hooked ) {
            add_action( 'wp_footer', [ $this, 'print_fallback_styles' ], 6 );
            self::$fallback_styles_hooked = true;
        }
    }

    /**
     * Resolve dashboard bundle src/version, preferring the Pro plugin when available.
     */
    private function resolve_pro_dashboard_script( string $relative_path, string $fallback_url, string $fallback_version ): array
    {
        $src       = $fallback_url;
        $ver       = $fallback_version;
        $lite_path = BOOKARO_PLUGIN_DIR . ltrim( $relative_path, '/' );

        if (
            function_exists( 'bookaro_is_pro_active' ) &&
            bookaro_is_pro_active() &&
            defined( 'BOOKARO_PRO_PLUGIN_DIR' ) &&
            defined( 'BOOKARO_PRO_PLUGIN_URL' )
        ) {
            $pro_path = BOOKARO_PRO_PLUGIN_DIR . ltrim( $relative_path, '/' );
            if ( file_exists( $pro_path ) ) {
                $src = BOOKARO_PRO_PLUGIN_URL . ltrim( $relative_path, '/' );
                // Bug 47: customers running the same plugin version after a
                // patch rebuild were stuck on the cached `?ver=1.1.0` bundle
                // because BOOKARO_(PRO_)VERSION is bumped infrequently while
                // the bundle is rebuilt on every fix. Use filemtime() so any
                // rebuild instantly cache-busts (matching the pattern already
                // used in SettingsPageController / SystemStatusController).
                $ver = (string) filemtime( $pro_path );
            }
        } elseif ( file_exists( $lite_path ) ) {
            // Lite fallback — same Bug 47 rationale.
            $ver = (string) filemtime( $lite_path );
        }

        return [ $src, $ver ];
    }

    /**
     * Enqueue vendor assets needed for reports (Chartist) when Pro reports are enabled.
     */
    private function maybe_enqueue_reports_assets( array &$deps ): void
    {
        if ( function_exists( 'bookaro_pro_enqueue_reports_assets' ) ) {
            bookaro_pro_enqueue_reports_assets( $deps );
        }
    }

    /**
     * Build frontend settings payload for reports UI.
     */
    private function get_reports_frontend_data(): array
    {
        if ( function_exists( 'bookaro_pro_get_reports_frontend_data' ) ) {
            return bookaro_pro_get_reports_frontend_data();
        }

        return [];
    }

    /**
     * Build frontend settings payload for payments UI.
     */
    private function get_payments_frontend_data(): array
    {
        $data = [
            'enabled' => false,
        ];

        $filtered = apply_filters( 'bookaro_payments_frontend_data', $data );
        if ( is_array( $filtered ) ) {
            return $filtered;
        }

        return $data;
    }

    /**
     * Build frontend setup checklist data for providers.
     */
    private function get_provider_setup_data( \WP_User $user, string $integration_mode, ?array $timeslots = null, ?array $listing_ids = null ): array
    {
        $provider_id = (int) $user->ID;
        if ( $provider_id < 1 ) {
            return [];
        }
        $site_type = function_exists( 'bookaro_get_site_type' )
            ? sanitize_key( (string) bookaro_get_site_type() )
            : 'directory';
        if ( ! in_array( $site_type, [ 'directory', 'single_business', 'advanced_mapping' ], true ) ) {
            $site_type = 'directory';
        }

        if ( null === $timeslots ) {
            $schedule  = new ScheduleService( $this->db );
            $timeslots = $schedule->get_timeslots( $provider_id );
        }

        $has_timeslots = ! empty( $timeslots );
        $has_bookings = $this->db->count_total( $provider_id ) > 0;

        $listing_url = '';
        $edit_listing_url = '';
        $has_services = false;

        if ( null === $listing_ids ) {
            $listing_ids = $this->get_provider_listing_ids( $provider_id );
        }
        $browse_url = $this->resolve_setup_browse_url( $listing_ids );
        $create_listing_url = $this->resolve_setup_create_listing_url( $listing_ids );
        foreach ( $listing_ids as $listing_id ) {
            $listing_id = (int) $listing_id;
            if ( $listing_id < 1 ) {
                continue;
            }
            if ( '' === $listing_url ) {
                $permalink = get_permalink( $listing_id );
                if ( is_string( $permalink ) ) {
                    $listing_url = $permalink;
                }
            }
            if ( '' === $edit_listing_url ) {
                $edit_link = get_edit_post_link( $listing_id, '' );
                if ( is_string( $edit_link ) ) {
                    $edit_listing_url = $edit_link;
                }
            }

            $services = [];
            if ( 'generic' === $integration_mode ) {
                if ( function_exists( 'bookaro_get_services_catalog_for_listing' ) ) {
                    $services = bookaro_get_services_catalog_for_listing( $listing_id );
                } elseif ( function_exists( 'bookaro_get_public_services_for_listing' ) ) {
                    $services = bookaro_get_public_services_for_listing( $listing_id );
                }
            } elseif ( function_exists( 'bookaro_resolve_listing_services' ) ) {
                $services = bookaro_resolve_listing_services( $listing_id, $integration_mode );
            }
            if ( empty( $services ) && function_exists( 'bookaro_get_bookaro_services_for_listing' ) ) {
                $services = bookaro_get_bookaro_services_for_listing( $listing_id );
            }
            if ( is_array( $services ) && ! empty( $services ) ) {
                $has_services = true;
            }
            if ( $has_services && $listing_url !== '' && $edit_listing_url !== '' ) {
                break;
            }
        }

        return [
            'site_type' => $site_type,
            'has_timeslots' => $has_timeslots,
            'has_services' => $has_services,
            'has_bookings' => $has_bookings,
            'has_listings' => ! empty( $listing_ids ),
            'listing_url' => $listing_url,
            'edit_listing_url' => $edit_listing_url,
            'create_listing_url' => $create_listing_url,
            'browse_url' => $browse_url,
            'listing_ids' => $listing_ids,
        ];
    }

    /**
     * Build frontend setup checklist data for customers.
     */
    private function get_owner_setup_data( \WP_User $user ): array
    {
        $owner_id = (int) $user->ID;
        if ( $owner_id < 1 ) {
            return [];
        }

        $has_bookings = $this->db->count_owner_total( $owner_id ) > 0;

        return [
            'has_bookings' => $has_bookings,
            'browse_url' => $this->resolve_setup_browse_url(),
        ];
    }

    /**
     * Resolve a user-facing browse URL for first-run actions.
     *
     * Prefer a listing archive when available; fall back to site home.
     */
    private function resolve_setup_browse_url( array $listing_ids = [] ): string
    {
        $url = '';
        foreach ( $listing_ids as $listing_id ) {
            $post_type = get_post_type( (int) $listing_id );
            if ( ! is_string( $post_type ) || $post_type === '' || 'page' === $post_type ) {
                continue;
            }
            $archive = get_post_type_archive_link( $post_type );
            if ( is_string( $archive ) && $archive !== '' ) {
                $url = $archive;
                break;
            }
        }

        if ( $url === '' && function_exists( 'bookaro_get_allowed_listing_post_types' ) ) {
            $types = bookaro_get_allowed_listing_post_types();
            if ( is_array( $types ) ) {
                foreach ( $types as $type ) {
                    $type = sanitize_key( (string) $type );
                    if ( $type === '' || 'page' === $type ) {
                        continue;
                    }
                    $archive = get_post_type_archive_link( $type );
                    if ( is_string( $archive ) && $archive !== '' ) {
                        $url = $archive;
                        break;
                    }
                }
            }
        }

        if ( $url === '' ) {
            $url = home_url( '/' );
        }

        /**
         * Filter setup browse URL shown in dashboard first-run actions.
         *
         * @param string $url
         * @param array<int> $listing_ids
         */
        $url = apply_filters( 'bookaro_setup_browse_url', $url, $listing_ids );

        return is_string( $url ) && $url !== '' ? $url : home_url( '/' );
    }

    /**
     * Resolve a provider-facing URL for creating the next listing.
     */
    private function resolve_setup_create_listing_url( array $listing_ids = [] ): string
    {
        $post_types = [];
        foreach ( $listing_ids as $listing_id ) {
            $post_type = get_post_type( (int) $listing_id );
            if ( ! is_string( $post_type ) || $post_type === '' || 'page' === $post_type ) {
                continue;
            }
            $post_types[] = sanitize_key( $post_type );
        }

        if ( empty( $post_types ) && function_exists( 'bookaro_get_allowed_listing_post_types' ) ) {
            $types = bookaro_get_allowed_listing_post_types();
            if ( is_array( $types ) ) {
                foreach ( $types as $type ) {
                    $type = sanitize_key( (string) $type );
                    if ( $type === '' || 'page' === $type ) {
                        continue;
                    }
                    $post_types[] = $type;
                }
            }
        }

        if ( empty( $post_types ) ) {
            $post_types = [ 'job_listing' ];
        }
        $post_types = array_values( array_unique( $post_types ) );

        $url = '';
        foreach ( $post_types as $post_type ) {
            $object = get_post_type_object( $post_type );
            if ( ! $object ) {
                continue;
            }

            $create_capability = 'edit_posts';
            if ( isset( $object->cap ) && is_object( $object->cap ) && ! empty( $object->cap->create_posts ) ) {
                $create_capability = (string) $object->cap->create_posts;
            }
            if ( ! current_user_can( $create_capability ) ) {
                continue;
            }

            $candidate = admin_url( 'post-new.php?post_type=' . $post_type );
            if ( is_string( $candidate ) && $candidate !== '' ) {
                $url = $candidate;
                break;
            }
        }

        /**
         * Filter setup create-listing URL shown in dashboard first-run actions.
         *
         * @param string $url
         * @param array<int> $listing_ids
         * @param array<int,string> $post_types
         */
        $url = apply_filters( 'bookaro_setup_create_listing_url', $url, $listing_ids, $post_types );

        return is_string( $url ) ? $url : '';
    }

    /**
     * Resolve listing ids for a provider using the same rules as provider services.
     */
    private function get_provider_listing_ids( int $user_id ): array
    {
        if ( $user_id < 1 || ! function_exists( 'bookaro_get_allowed_listing_post_types' ) ) {
            return [];
        }

        $types = bookaro_get_allowed_listing_post_types();
        if ( empty( $types ) ) {
            return [];
        }

        $args = [
            'post_type'      => $types,
            'post_status'    => [ 'publish', 'draft', 'pending', 'private' ],
            'posts_per_page' => 200,
            'orderby'        => 'title',
            'order'          => 'ASC',
            'fields'         => 'ids',
        ];

        $resolution_mode = function_exists( 'bookaro_get_option' )
            ? (string) bookaro_get_option( 'provider_resolution_mode', '' )
            : '';
        $resolution_mode = sanitize_key( $resolution_mode );
        $provider_meta_key = function_exists( 'bookaro_get_option' )
            ? (string) bookaro_get_option( 'provider_meta_key', '' )
            : '';
        $provider_meta_key = sanitize_key( $provider_meta_key );
        if ( $provider_meta_key === '' ) {
            $legacy_meta_key = function_exists( 'bookaro_get_option' )
                ? (string) bookaro_get_option( 'listing_provider_meta_key', '' )
                : '';
            $legacy_meta_key = sanitize_key( $legacy_meta_key );
            if ( $legacy_meta_key !== '' ) {
                $provider_meta_key = $legacy_meta_key;
            }
        }
        if ( ! in_array( $resolution_mode, [ 'author', 'meta_key' ], true ) ) {
            $resolution_mode = $provider_meta_key !== '' ? 'meta_key' : 'author';
        }
        if ( 'meta_key' === $resolution_mode && $provider_meta_key !== '' ) {
            $args['meta_query'] = [
                [
                    'key'   => $provider_meta_key,
                    'value' => $user_id,
                    'compare' => '=',
                    'type'  => 'NUMERIC',
                ],
            ];
        } else {
            $args['author'] = $user_id;
        }

        $args = apply_filters( 'bookaro_provider_listings_query_args', $args, $user_id );
        $ids = get_posts( $args );

        $listing_ids = [];
        foreach ( $ids as $listing_id ) {
            $listing_id = (int) $listing_id;
            if ( $listing_id < 1 ) {
                continue;
            }
            if ( function_exists( 'bookaro_is_allowed_listing' ) && ! bookaro_is_allowed_listing( $listing_id ) ) {
                continue;
            }
            if ( function_exists( 'bookaro_is_lite_listing_allowed' ) && ! bookaro_is_lite_listing_allowed( $listing_id ) ) {
                continue;
            }
            if ( ! $this->user_owns_listing( $listing_id, $user_id ) ) {
                continue;
            }
            $listing_ids[] = $listing_id;
        }

        return $listing_ids;
    }

    private function user_owns_listing( int $listing_id, int $user_id ): bool
    {
        if ( current_user_can( 'manage_options' ) ) {
            return true;
        }
        if ( ! function_exists( 'bookaro_get_listing_owner_id' ) ) {
            return false;
        }
        $owner_id = (int) bookaro_get_listing_owner_id( $listing_id );
        return $owner_id > 0 && $owner_id === $user_id;
    }

    /**
     * Build frontend settings payload for booking policy messaging.
     */
    private function get_policy_frontend_data(): array
    {
        return BookingPolicyPresenter::build_policy_bundle(
            [
                'cancel_window_hours' => absint( bookaro_get_option( 'booking_cancel_window_hours', 0 ) ),
                'reschedule_window_hours' => absint( bookaro_get_option( 'booking_reschedule_window_hours', 0 ) ),
                'cancel_template' => sanitize_text_field( (string) bookaro_get_option( 'booking_cancel_policy_text', '' ) ),
                'reschedule_template' => sanitize_text_field( (string) bookaro_get_option( 'booking_reschedule_policy_text', '' ) ),
            ]
        );
    }

    /**
     * Enqueue Day.js along with the site's locale and set it globally.
     */
    private function enqueue_dayjs_locale(): void
    {
        $locale = strtolower( determine_locale() );
        $locale = str_replace( '_', '-', $locale );

        wp_enqueue_script( 'dayjs', BOOKARO_PLUGIN_URL . 'assets/vendor/dayjs/dayjs.min.js', [], '1.11.10', true );

        if ( substr( $locale, 0, 2 ) !== 'en' ) {
            $locale_file = BOOKARO_PLUGIN_DIR . 'assets/vendor/dayjs/locale/' . $locale . '.js';
            if ( file_exists( $locale_file ) ) {
                wp_enqueue_script(
                    'dayjs-locale',
                    BOOKARO_PLUGIN_URL . 'assets/vendor/dayjs/locale/' . $locale . '.js',
                    [ 'dayjs' ],
                    '1.11.10',
                    true
                );
                wp_add_inline_script( 'dayjs-locale', 'dayjs.locale(' . wp_json_encode( $locale ) . ');' );
            } else {
                wp_add_inline_script( 'dayjs', "dayjs.locale('en');" );
            }
        } else {
            wp_add_inline_script( 'dayjs', "dayjs.locale('en');" );
        }
    }

    /**
     * Resolve a typography preset (plus optional custom stack) into a CSS
     * font-family value. Backward compatible with sites saved before the
     * preset selector existed (raw stack stored in appearance_*_font with no
     * *_preset key). For headings, an empty return means "inherit body font".
     *
     * @param array<string,mixed> $opts
     */
    private function resolve_appearance_font( array $opts, string $which ): string
    {
        $is_heading = ( 'heading' === $which );
        $preset_key = 'appearance_' . $which . '_font_preset';
        $custom_key = 'appearance_' . $which . '_font';
        $custom     = trim( (string) ( $opts[ $custom_key ] ?? '' ) );

        $map = [
            'theme'  => 'inherit',
            'system' => 'system-ui, sans-serif',
            'sans'   => 'Arial, Helvetica, sans-serif',
            'serif'  => 'Georgia, Cambria, serif',
            'mono'   => 'ui-monospace, Menlo, Consolas, monospace',
        ];

        // Backward compatibility: sites saved before the preset selector
        // existed only have the raw font stack in $custom_key (no *_preset key).
        if ( ! array_key_exists( $preset_key, $opts ) ) {
            if ( $is_heading ) {
                return $custom; // empty => headings inherit the body font.
            }
            return ( '' !== $custom ) ? $custom : 'system-ui, sans-serif';
        }

        $preset = sanitize_key( (string) ( $opts[ $preset_key ] ?? '' ) );

        // Headings: "Same as body" => empty => inherit the body font.
        if ( $is_heading && ( 'body' === $preset || '' === $preset ) ) {
            return '';
        }

        if ( 'custom' === $preset ) {
            if ( '' !== $custom ) {
                return $custom;
            }
            return $is_heading ? '' : 'system-ui, sans-serif';
        }

        if ( isset( $map[ $preset ] ) ) {
            return $map[ $preset ];
        }

        return $is_heading ? '' : 'system-ui, sans-serif';
    }

    /**
     * Inject primary color and font customizations inline in <head>.
     */
    public function print_custom_styles(): void
    {

        $opts = get_option( 'bookaro_settings', [] );

        // Step 1: Define all CSS variables
        $max_width = trim( (string) ( $opts['appearance_modal_max_width'] ?? '' ) );
        if ( $max_width === '' || $max_width === 'min(600px,90%)' || $max_width === 'min(600px, 90%)' ) {
            $max_width = 'min(96vw, 86rem)';
        }

        $vars = [
            'accent'       => sanitize_hex_color( $opts['appearance_primary_color']        ?? '#28a745' ),
            'secondary'    => sanitize_hex_color( $opts['appearance_secondary_color']      ?? '#6c757d' ),
            'overlay'      => esc_attr( $opts['appearance_overlay_color']                  ?? 'rgba(0,0,0,0.5)' ),
            'modal_bg'     => sanitize_hex_color( $opts['appearance_modal_bg_color']       ?? '#ffffff' ),
            'close'        => sanitize_hex_color( $opts['appearance_close_btn_color']      ?? '#000000' ),
            'close_hover'  => sanitize_hex_color( $opts['appearance_close_btn_hover_color']?? '#333333' ),
            'body_font'    => esc_attr( $this->resolve_appearance_font( $opts, 'body' ) ),
            'heading_font' => esc_attr( $this->resolve_appearance_font( $opts, 'heading' ) ),
            'max_width'    => esc_attr( $max_width ),
            'dashboard_max_width' => esc_attr( $opts['appearance_dashboard_max_width']    ?? '800px' ),
            'padding'      => esc_attr( $opts['appearance_modal_padding']                  ?? '2rem' ),
            'radius'       => esc_attr( $opts['appearance_border_radius']                  ?? '1.5rem' ),
            'shadow_op'    => floatval( $opts['appearance_shadow_opacity']                 ?? 0.1 ),
            'cal_head'     => sanitize_hex_color( $opts['appearance_calendar_header_color']?? '#f7f7f7' ),
            'slot_border'  => sanitize_hex_color( $opts['appearance_slot_border_color']    ?? '#dee2e6' ),
            'slot_sel'     => sanitize_hex_color( $opts['appearance_slot_selected_color']  ?? '#007cba' ),
            'slot_dis'     => sanitize_hex_color( $opts['appearance_slot_disabled_color']  ?? '#cccccc' ),
        ];

        $action_vars = [
            'approve'    => sanitize_hex_color( $opts['appearance_action_approve_color']    ?? '#22c55e' ),
            'reject'     => sanitize_hex_color( $opts['appearance_action_reject_color']     ?? '#ef4444' ),
            'view'       => sanitize_hex_color( $opts['appearance_action_view_color']       ?? '#2563eb' ),
            'note'       => sanitize_hex_color( $opts['appearance_action_note_color']       ?? '#0ea5e9' ),
            'cancel'     => sanitize_hex_color( $opts['appearance_action_cancel_color']     ?? '#dc2626' ),
            'reschedule' => sanitize_hex_color( $opts['appearance_action_reschedule_color'] ?? '#6366f1' ),
            'complete'   => sanitize_hex_color( $opts['appearance_action_complete_color']   ?? '#16a34a' ),
        ];

        $dashboard_density = sanitize_key( $opts['appearance_dashboard_density'] ?? 'comfortable' );
        if ( ! in_array( $dashboard_density, [ 'comfortable', 'compact' ], true ) ) {
            $dashboard_density = 'comfortable';
        }
        $dashboard_surface_style = sanitize_key( $opts['appearance_dashboard_surface_style'] ?? 'soft' );
        if ( ! in_array( $dashboard_surface_style, [ 'soft', 'flat' ], true ) ) {
            $dashboard_surface_style = 'soft';
        }

        $accent = $vars['accent'] ?: '#007cba';
        $secondary = $vars['secondary'] ?: '#6c757d';
        $accent_rgb = $this->hex_to_rgb( $accent ) ?? '0, 124, 186';
        $secondary_rgb = $this->hex_to_rgb( $secondary ) ?? '108, 117, 125';
        $shadow_op = max( 0, min( 1, floatval( $vars['shadow_op'] ) ) );
        $dashboard_shadow_op = isset( $opts['appearance_dashboard_shadow_opacity'] )
            ? max( 0, min( 1, floatval( $opts['appearance_dashboard_shadow_opacity'] ) ) )
            : $shadow_op;
        $dashboard_shadow_depth = sanitize_key( $opts['appearance_dashboard_shadow_depth'] ?? 'medium' );
        if ( ! in_array( $dashboard_shadow_depth, [ 'subtle', 'medium', 'deep' ], true ) ) {
            $dashboard_shadow_depth = 'medium';
        }
        $dashboard_shadow_presets = [
            'subtle' => '0 8px 18px rgba(15, 23, 42, %s)',
            'medium' => '0 18px 40px rgba(15, 23, 42, %s)',
            'deep' => '0 28px 60px rgba(15, 23, 42, %s)',
        ];
        $dashboard_shadow_value = sprintf(
            $dashboard_shadow_presets[ $dashboard_shadow_depth ] ?? $dashboard_shadow_presets['medium'],
            $dashboard_shadow_op
        );
        $dashboard_shadow = $dashboard_surface_style === 'flat' ? 'none' : $dashboard_shadow_value;

        $dashboard_border = sanitize_hex_color( $opts['appearance_dashboard_border_color'] ?? '#e2e8f0' );
        if ( empty( $dashboard_border ) ) {
            $dashboard_border = '#e2e8f0';
        }
        $dashboard_border_width = absint( $opts['appearance_dashboard_border_width'] ?? 1 );
        if ( $dashboard_border_width > 6 ) {
            $dashboard_border_width = 6;
        }

        $dashboard_vars = [
            'accent' => $accent,
            'accent-rgb' => $accent_rgb,
            'secondary' => $secondary,
            'secondary-rgb' => $secondary_rgb,
            'text' => '#0f172a',
            'muted' => '#64748b',
            'bg' => '#f8fafc',
            'surface' => '#ffffff',
            'border' => $dashboard_border,
            'border-width' => $dashboard_border_width . 'px',
            'shadow' => $dashboard_shadow,
            'radius' => $vars['radius'] ?: '1.5rem',
        ];
        if ( $dashboard_density === 'compact' ) {
            $dashboard_vars['pad'] = '1rem';
            $dashboard_vars['gap'] = '0.75rem';
            $dashboard_vars['card-pad'] = '1rem';
            $dashboard_vars['row-pad'] = '0.5rem';
            $dashboard_vars['pill-pad'] = '0.2rem 0.5rem';
        } else {
            $dashboard_vars['pad'] = '1.5rem';
            $dashboard_vars['gap'] = '1rem';
            $dashboard_vars['card-pad'] = '1.25rem';
            $dashboard_vars['row-pad'] = '0.75rem';
            $dashboard_vars['pill-pad'] = '0.3rem 0.65rem';
        }
        $dashboard_vars['pad-base'] = $dashboard_vars['pad'];
        $dashboard_vars['gap-base'] = $dashboard_vars['gap'];
        $dashboard_vars['card-pad-base'] = $dashboard_vars['card-pad'];
        $dashboard_vars['row-pad-base'] = $dashboard_vars['row-pad'];
        $dashboard_vars['pill-pad-base'] = $dashboard_vars['pill-pad'];

        // Step 2: Start CSS output
        $scope_selector = ':is(.bookaro-scope,#bookaro-booking-app,#bookaro-provider-dashboard,#bookaro-owner-dashboard)';
        $css = "{$scope_selector} {\n";
        foreach ( $vars as $key => $val ) {
            if ( $key === 'heading_font' && ! $val ) continue;
            $css .= "  --bookaro-booking-{$key}: {$val};\n";
        }
        foreach ( $action_vars as $key => $val ) {
            if ( empty( $val ) ) {
                continue;
            }
            $css .= "  --bookaro-action-{$key}: {$val};\n";
        }
        foreach ( $dashboard_vars as $key => $val ) {
            if ( $val === '' ) {
                continue;
            }
            $css .= "  --bookaro-dashboard-{$key}: {$val};\n";
        }
        $css .= "}\n";

        $css .= "
          .bookaro-scope .flatpickr-months,
          .bookaro-scope .flatpickr-weekdays {
            background-color: var(--bookaro-booking-cal_head) !important;
          }
        ";

        // Step 3: Old style rules
        $css .= "
    .bookaro-scope .bookaro-booking-btn,
    .bookaro-scope .bookaro-booking-launch,
    .bookaro-booking-btn.bookaro-scope,
    .bookaro-booking-launch.bookaro-scope {
        background-color: var(--bookaro-booking-accent) !important;
        border-color:     var(--bookaro-booking-accent) !important;
    }
    .bookaro-scope .bookaro-booking-btn-secondary,
    .bookaro-booking-btn-secondary.bookaro-scope {
        background: var(--bookaro-booking-secondary) !important;
        border-color: var(--bookaro-booking-secondary) !important;
    }
    .bookaro-scope .date-pill.selected {
        background: var(--bookaro-booking-accent) !important;
    }
    .bookaro-scope .date-pill.range-start,
    .bookaro-scope .date-pill.range-end {
        background: var(--bookaro-booking-accent) !important;
    }
    .bookaro-scope .date-pill.in-range {
        background: var(--bookaro-booking-accent) !important;
        opacity: 0.5;
    }
    .bookaro-scope .modal-close-button {
        background: var(--bookaro-booking-close) !important;
    }
    .bookaro-scope .modal-close-button:hover {
        background: var(--bookaro-booking-close_hover) !important;
    }
    .bookaro-scope .bookaro-booking-modal {
        background: var(--bookaro-booking-overlay) !important;
    }
    .bookaro-scope .bookaro-booking-wrapper {
        background: var(--bookaro-booking-modal_bg);
        max-width: var(--bookaro-booking-max_width) !important;
        padding: var(--bookaro-booking-padding) !important;
        border-radius: var(--bookaro-booking-radius) !important;
        box-shadow: 0 10px 40px rgba(0,0,0,var(--bookaro-booking-shadow_op));
        font-family: var(--bookaro-booking-body_font);
    }
    .bookaro-scope .bookaro-booking-wrapper h3,
    #bookaro-provider-dashboard.bookaro-scope h2 {
        " . ( $vars['heading_font'] ? "font-family: var(--bookaro-booking-heading_font) !important;" : '' ) . "
    }

    /* Calendar */
    .bookaro-scope .date-pill.unavailable {
        color: #ccc !important;
    }

    /* Timeslots */
    .bookaro-scope .slot-grid button {
        border-color: var(--bookaro-booking-slot_border) !important;
    }
    .bookaro-scope .slot-grid button.selected {
        background: var(--bookaro-booking-slot_sel) !important;
        color: #fff !important;
    }
    .bookaro-scope .slot-grid button.disabled {
        background: var(--bookaro-booking-slot_dis) !important;
        color: #666 !important;
    }

    #pbs-notice-wrapper.bookaro-scope {
      position: fixed;
      top: 9rem;
      right: 1rem;
      z-index: 9998;
      transition: transform 0.4s ease;
    }
    #pbs-notice-wrapper.bookaro-scope.pbs-hidden {
      transform: translateX(101%);
    }
    #pbs-notice-wrapper.bookaro-scope #pbs-notice-toggle {
      position: absolute;
      left: -1.75rem;
      top: 0;
      width: 1.75rem;
      height: 2.6rem;
      background: var(--bookaro-booking-accent);
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 8px 0 0 8px;
      cursor: pointer;
      transition: background 0.3s ease, transform 0.4s ease;
      z-index: 1;
      border: none;
      animation: bookaro-slide-in-right 0.6s ease-out;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer {
      width: 365px;
      padding: 0.85rem;
      background: #fff;
      border: 1px solid rgba(0, 0, 0, 0.08);
      box-shadow: 0 16px 30px rgba(0, 0, 0, 0.12);
      border-radius: 0.9rem;
      border-top-left-radius: 0;
      font-family: var(--bookaro-booking-body_font);
      animation: bookaro-slide-in-right 0.6s ease-out;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__header {
      display: flex;
      flex-direction: column;
      gap: 0.4rem;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__title {
      margin: 0;
      font-size: 0.95rem;
      font-weight: 700;
      color: #111;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__subtitle {
      margin: 0;
      font-size: 0.78rem;
      color: #666;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__progress {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-top: 0.4rem;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__label {
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: #777;
      font-weight: 600;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__bar {
      width: 100%;
      height: 6px;
      border-radius: 999px;
      background: rgba(0, 0, 0, 0.08);
      overflow: hidden;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__fill {
      display: block;
      height: 100%;
      background: var(--bookaro-booking-accent);
      border-radius: inherit;
      transition: width 0.3s ease;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-drawer__list {
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
      margin-top: 0.75rem;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item {
      display: flex;
      align-items: flex-start;
      gap: 0.5rem;
      padding: 0.5rem 0.6rem;
      border-radius: 0.7rem;
      border: 1px solid #eef2f7;
      background: #f8fafc;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item.is-complete {
      opacity: 0.7;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item__badge {
      width: 1.4rem;
      height: 1.4rem;
      border-radius: 999px;
      background: #fff;
      border: 1px solid #e2e8f0;
      font-size: 0.7rem;
      font-weight: 700;
      color: #777;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item.is-complete .bookaro-setup-item__badge {
      background: var(--bookaro-booking-accent);
      border-color: var(--bookaro-booking-accent);
      color: #fff;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item__title {
      margin: 0;
      font-size: 0.82rem;
      font-weight: 600;
      color: #111;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item__desc {
      margin: 0.2rem 0 0;
      font-size: 0.75rem;
      color: #666;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item__action {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 100%;
      margin-top: 0.45rem;
      padding: 0.4rem 0.8rem;
      background: #fff;
      color: var(--bookaro-booking-accent);
      text-decoration: none;
      font-weight: 600;
      font-size: 0.72rem;
      border-radius: 0.7rem;
      border: 1px solid rgba(0, 0, 0, 0.08);
      transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
    }

    #pbs-notice-wrapper.bookaro-scope .bookaro-setup-item__action:hover {
      color: #fff;
      background: var(--bookaro-booking-accent);
      border-color: var(--bookaro-booking-accent);
    }

@keyframes bookaro-slide-in-right {
  from {
    opacity: 0;
    transform: translateX(300px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

    ";

        $css = str_replace( '.bookaro-scope ', $scope_selector . ' ', $css );
        $css = str_replace( '#bookaro-provider-dashboard.bookaro-scope h2', '#bookaro-provider-dashboard h2', $css );

        // Attach the appearance CSS as inline data on a dedicated, src-less style
        // handle instead of printing an inline style element. The values
        // interpolated into $css are already sanitized above (sanitize_hex_color/esc_attr/
        // absint/floatval), so the CSS is passed through unescaped as inline
        // style data, which is the expected wp_add_inline_style() contract.
        $handle = 'bookaro-appearance';
        if ( ! wp_style_is( $handle, 'registered' ) ) {
            wp_register_style( $handle, false, [], BOOKARO_VERSION );
        }
        wp_enqueue_style( $handle );
        wp_add_inline_style( $handle, $css );
    }

    /**
     * Convert a hex color to a RGB string (e.g., "34, 197, 94").
     */
    private function hex_to_rgb( string $hex ): ?string
    {
        $hex = sanitize_hex_color( $hex );
        if ( empty( $hex ) ) {
            return null;
        }
        $hex = ltrim( $hex, '#' );
        if ( strlen( $hex ) === 3 ) {
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
        }
        if ( strlen( $hex ) !== 6 ) {
            return null;
        }
        $int = hexdec( $hex );
        $r = ( $int >> 16 ) & 255;
        $g = ( $int >> 8 ) & 255;
        $b = $int & 255;
        return "{$r}, {$g}, {$b}";
    }
}
