<?php
/**
 * Plugin Name:       MyBookaro
 * Plugin URI:        https://mybookaro.com/
 * Description:       Request-to-book, availability, dashboards, reminders, and lane-aware setup for provider-based WordPress sites.
 * Version:           1.2.1
 * Requires at least: 6.0
 * Requires PHP:      8.1
 * License:           GPLv2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Author:            MyBookaro
 * Author URI:        https://mybookaro.com
 * Text Domain:       mybookaro
 * Domain Path:       /languages
 */

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- meta_query is required to discover bookable listings during bootstrap and is bounded by the active lane/context.

// Define a constant for plugin file location, used by activation hooks in Loader
if ( ! defined( 'BOOKARO_PLUGIN_FILE' ) ) {
    define( 'BOOKARO_PLUGIN_FILE', __FILE__ );
}

if ( ! defined( 'BOOKARO_VERSION' ) ) {
    define( 'BOOKARO_VERSION', '1.2.1' );
}

if ( ! defined( 'BOOKARO_MIN_PHP' ) ) {
    define( 'BOOKARO_MIN_PHP', '8.1' );
}

if ( ! defined( 'BOOKARO_DB_VERSION' ) ) {
    define( 'BOOKARO_DB_VERSION', '7' );
}

if ( ! defined( 'BOOKARO_PLUGIN_DIR' ) ) {
    define( 'BOOKARO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
}

if ( ! defined( 'BOOKARO_PLUGIN_URL' ) ) {
    define( 'BOOKARO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}

// Translation loading.
// Since WordPress 4.6, plugins hosted on .org have their translations
// loaded automatically from translate.wordpress.org based on the
// `Text Domain:` and `Domain Path:` headers above — no manual
// load_plugin_textdomain() call is required (Plugin Check flags it
// as discouraged). For local/private installs the Domain Path still
// applies, so packaged .mo files in /languages keep working.

if ( ! defined( 'BOOKARO_DEFAULT_UPGRADE_URL' ) ) {
    define( 'BOOKARO_DEFAULT_UPGRADE_URL', 'https://mybookaro.com/pricing' );
}

if ( ! defined( 'BOOKARO_PROVIDER_POST_TYPE' ) ) {
    define( 'BOOKARO_PROVIDER_POST_TYPE', 'bookaro_provider' );
}

if ( ! defined( 'BOOKARO_PROVIDER_EMAIL_META' ) ) {
    define( 'BOOKARO_PROVIDER_EMAIL_META', 'bookaro_provider_email' );
}

if ( ! defined( 'BOOKARO_PROVIDER_PHONE_META' ) ) {
    define( 'BOOKARO_PROVIDER_PHONE_META', 'bookaro_provider_phone' );
}

if ( ! defined( 'BOOKARO_SERVICE_POST_TYPE' ) ) {
    define( 'BOOKARO_SERVICE_POST_TYPE', 'bookaro_service' );
}

if ( ! defined( 'BOOKARO_SERVICE_PRICE_META' ) ) {
    define( 'BOOKARO_SERVICE_PRICE_META', 'bookaro_service_price' );
}

if ( ! defined( 'BOOKARO_SERVICE_DURATION_META' ) ) {
    define( 'BOOKARO_SERVICE_DURATION_META', 'bookaro_service_duration' );
}
if ( ! defined( 'BOOKARO_SERVICE_DURATION_MINUTES_META' ) ) {
    define( 'BOOKARO_SERVICE_DURATION_MINUTES_META', 'bookaro_service_duration_minutes' );
}
if ( ! defined( 'BOOKARO_SERVICE_LABEL_META' ) ) {
    define( 'BOOKARO_SERVICE_LABEL_META', 'bookaro_service_label' );
}
if ( ! defined( 'BOOKARO_SERVICE_DESCRIPTION_META' ) ) {
    define( 'BOOKARO_SERVICE_DESCRIPTION_META', 'bookaro_service_description' );
}

if ( ! defined( 'BOOKARO_SERVICE_LISTING_META' ) ) {
    define( 'BOOKARO_SERVICE_LISTING_META', 'bookaro_service_listing_id' );
}
if ( ! defined( 'BOOKARO_SERVICE_SCOPE_META' ) ) {
    define( 'BOOKARO_SERVICE_SCOPE_META', 'bookaro_service_scope' );
}
if ( ! defined( 'BOOKARO_SERVICE_PROVIDER_META' ) ) {
    define( 'BOOKARO_SERVICE_PROVIDER_META', 'bookaro_service_provider_id' );
}
if ( ! defined( 'BOOKARO_SERVICE_OWNER_META' ) ) {
    define( 'BOOKARO_SERVICE_OWNER_META', 'bookaro_service_owner_type' );
}
if ( ! defined( 'BOOKARO_SERVICE_SHARED_META' ) ) {
    define( 'BOOKARO_SERVICE_SHARED_META', 'bookaro_service_shared' );
}
if ( ! defined( 'BOOKARO_SERVICE_IS_SHARED_META' ) ) {
    define( 'BOOKARO_SERVICE_IS_SHARED_META', 'bookaro_service_is_shared' );
}
if ( ! defined( 'BOOKARO_SERVICES_CACHE_META' ) ) {
    define( 'BOOKARO_SERVICES_CACHE_META', 'bookaro_services_cache_buster' );
}
if ( ! defined( 'BOOKARO_CAP_PROVIDER' ) ) {
    define( 'BOOKARO_CAP_PROVIDER', 'bookaro_provider' );
}
if ( ! defined( 'BOOKARO_CAP_CUSTOMER' ) ) {
    define( 'BOOKARO_CAP_CUSTOMER', 'bookaro_customer' );
}

if ( ! bookaro_check_php_requirement() ) {
    return;
}

// Load Composer autoloader if present; otherwise fall back to a tiny PSR-4 loader for Bookaro\.
$bookaro_autoload = __DIR__ . '/vendor/autoload.php';
if ( file_exists( $bookaro_autoload ) ) {
    require_once $bookaro_autoload;
    $bookaro_autoload_available = true;
} else {
    $bookaro_autoload_available = true;
    spl_autoload_register( static function( $class ): void {
        $prefix = 'Bookaro\\';
        if ( strncmp( $prefix, $class, strlen( $prefix ) ) !== 0 ) {
            return;
        }
        $relative = substr( $class, strlen( $prefix ) );
        $relative = str_replace( '\\', '/', $relative );
        $file = __DIR__ . '/includes/src/' . $relative . '.php';
        if ( file_exists( $file ) ) {
            require_once $file;
        }
    } );
}
if ( ! defined( 'BOOKARO_AUTOLOAD_AVAILABLE' ) ) {
    define( 'BOOKARO_AUTOLOAD_AVAILABLE', $bookaro_autoload_available );
}

use Bookaro\Core\Loader;

/**
 * Resolve Bookaro service locator.
 */
function bookaro_services(): \Bookaro\Service\ServiceLocator {
    static $services = null;
    if ( null === $services ) {
        $services = new \Bookaro\Service\ServiceLocator();
    }
    return $services;
}

/**
 * Activation handler to enforce required dependencies.
 */
function bookaro_activate(): void {
    if ( defined( 'BOOKARO_AUTOLOAD_AVAILABLE' ) && ! BOOKARO_AUTOLOAD_AVAILABLE ) {
        wp_die(
            esc_html__( 'MyBookaro is missing required dependencies (vendor/). Please run composer install or reinstall the plugin package, then activate again.', 'mybookaro' )
        );
    }

    Loader::activate_static();

    if ( class_exists( '\\Bookaro\\Utils\\Logger' ) ) {
        \Bookaro\Utils\Logger::ensure_log_file( false );
    }

    // Welcome admin notice will be shown by GettingStartedController on the
    // next admin page load. We intentionally do NOT auto-redirect after
    // activation — WordPress.org Plugin Guideline 11 disallows hijacking
    // the admin flow on activation. Admins find Getting Started via the
    // notice or the MyBookaro menu.
}

/**
 * Deferred rewrite-rules flush. Loader::run_activation() runs a first flush
 * during plugin activation, but WC My Account endpoints registered by
 * AccountEndpointsController hook on `init` — they're not in $wp_rewrite at
 * activation time, so the first flush doesn't capture them. This handler
 * runs once on the next request after `init` (priority 99 so all endpoint
 * controllers have registered), re-flushes, and clears the flag.
 */
function bookaro_maybe_flush_rewrite_rules_after_activation(): void {
    if ( get_option( 'bookaro_needs_rewrite_flush' ) !== '1' ) {
        return;
    }
    delete_option( 'bookaro_needs_rewrite_flush' );
    flush_rewrite_rules( false );
}
add_action( 'init', 'bookaro_maybe_flush_rewrite_rules_after_activation', 99 );

// Setup activation/deactivation hooks immediately
register_activation_hook(__FILE__, 'bookaro_activate');
register_deactivation_hook(__FILE__, [Loader::class, 'deactivate_static']);


/**
 * Kick off the plugin.
 */
function bookaro_init(): void {
    if ( defined( 'BOOKARO_AUTOLOAD_AVAILABLE' ) && ! BOOKARO_AUTOLOAD_AVAILABLE ) {
        return;
    }

    bookaro_register_runtime_hooks();
    if ( defined( 'BOOKARO_PLUGIN_DIR' ) ) {
        require_once BOOKARO_PLUGIN_DIR . 'includes/lite-bootstrap.php';
    }
    // Instantiating Loader registers all hooks, activation/deactivation, etc.
    try {
        new Loader();
        bookaro_maybe_disable_payments_without_woocommerce();
    } catch ( \Throwable $e ) {
        if ( is_admin() ) {
            add_action( 'admin_notices', static function() {
                $message = __( 'MyBookaro failed to start due to an internal error. Please check server logs and contact support if the issue persists.', 'mybookaro' );
                echo '<div class="notice notice-error"><p>' . esc_html( $message ) . '</p></div>';
            } );
        }
        if ( function_exists( 'bookaro_get_option' ) && bookaro_get_option( 'log_php_errors', 0 ) ) {
            if ( class_exists( '\\Bookaro\\Utils\\Logger' ) ) {
                \Bookaro\Utils\Logger::log( 'bookaro_loader_boot_failed', [
                    'message' => $e->getMessage(),
                    'file'    => $e->getFile(),
                    'line'    => $e->getLine(),
                    'trace'   => $e->getTraceAsString(),
                ], true );
            }
        }
        return;
    }
}

// Initialize on plugins_loaded to ensure WP core is ready
add_action( 'plugins_loaded', 'bookaro_init' );

function bookaro_check_php_requirement(): bool {
    if ( PHP_VERSION_ID >= 80100 ) {
        return true;
    }

    add_action( 'admin_notices', 'bookaro_php_notice' );
    if ( function_exists( 'deactivate_plugins' ) ) {
        deactivate_plugins( plugin_basename( BOOKARO_PLUGIN_FILE ) );
    }
    return false;
}

function bookaro_php_notice(): void {
    if ( ! current_user_can( 'activate_plugins' ) ) {
        return;
    }
    $message = sprintf(
        /* translators: 1: required PHP version, 2: current PHP version */
        __( 'MyBookaro requires PHP %1$s or higher. Your site is running PHP %2$s. The plugin has been deactivated.', 'mybookaro' ),
        defined( 'BOOKARO_MIN_PHP' ) ? BOOKARO_MIN_PHP : '8.1',
        PHP_VERSION
    );
    echo '<div class="notice notice-error"><p>' . esc_html( $message ) . '</p></div>';
}

/**
 * Add a Settings link to the plugin row.
 *
 * @param array $links Existing action links.
 * @return array
 */
function bookaro_plugin_action_links( array $links ): array {
    if ( function_exists( 'bookaro_current_user_can_manage_admin' ) ) {
        if ( ! bookaro_current_user_can_manage_admin() ) {
            return $links;
        }
    } elseif ( ! current_user_can( 'manage_options' ) ) {
        return $links;
    }
    $settings_url = admin_url( 'admin.php?page=bookaro-booking-settings' );
    $settings_link = '<a href="' . esc_url( $settings_url ) . '">' . esc_html__( 'Settings', 'mybookaro' ) . '</a>';
    array_unshift( $links, $settings_link );
    return $links;
}
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'bookaro_plugin_action_links' );

/**
 * Disable payments if the configured gateway is unavailable.
 */
function bookaro_maybe_disable_payments_without_woocommerce(): void {
    if ( function_exists( 'bookaro_get_payments_gateway' ) ) {
        $gateway = bookaro_get_payments_gateway();
        if ( $gateway && $gateway->is_available() ) {
            return;
        }
    }
    if ( ! function_exists( 'bookaro_get_option' ) ) {
        return;
    }
    $payments_enabled = (bool) bookaro_get_option( 'payments_enabled', 0 );
    if ( ! $payments_enabled ) {
        return;
    }

    $repo = new \Bookaro\Repository\OptionRepository();
    $settings = $repo->get_all();
    $settings['payments_enabled'] = 0;
    $repo->update( $settings );

    add_action( 'admin_notices', 'bookaro_payments_unavailable_notice' );
}

/**
 * Check whether Bookaro Pro is active.
 */
function bookaro_is_pro_active(): bool {
    $active = defined( 'BOOKARO_PRO_ACTIVE' ) && BOOKARO_PRO_ACTIVE;
    return (bool) apply_filters( 'bookaro_is_pro_active', $active );
}

/**
 * Check whether Bookaro frontend output is enabled.
 */
function bookaro_is_frontend_enabled(): bool {
    $enabled = true;
    $settings = get_option( 'bookaro_settings', [] );
    if ( is_array( $settings ) && array_key_exists( 'frontend_enabled', $settings ) ) {
        $enabled = ! empty( $settings['frontend_enabled'] );
    }
    return (bool) apply_filters( 'bookaro_frontend_enabled', $enabled );
}

/**
 * Feature gate for optional capabilities.
 *
 * @param string $feature Feature key (e.g. "mylisting").
 */
function bookaro_feature_enabled( string $feature ): bool {
    $feature = sanitize_key( $feature );
    $pro_features  = [ 'gcal', 'messages', 'sms', 'payments', 'webhooks', 'reports', 'exports', 'chat', 'staff', 'custom_forms' ];
    $lite_features = [ 'mylisting' ];
    if ( in_array( $feature, $pro_features, true ) ) {
        $enabled = false;
    } else {
        $enabled = in_array( $feature, $lite_features, true );
    }
    return (bool) apply_filters( 'bookaro_feature_enabled', $enabled, $feature );
}

/**
 * Set the active payments gateway instance.
 */
function bookaro_set_payments_gateway( \Bookaro\Contracts\PaymentsGatewayInterface $gateway ): void {
    if ( function_exists( 'bookaro_services' ) ) {
        bookaro_services()->set_payments( $gateway );
        return;
    }
    $GLOBALS['bookaro_payments_gateway'] = $gateway;
}

/**
 * Get the active payments gateway instance.
 */
function bookaro_get_payments_gateway(): \Bookaro\Contracts\PaymentsGatewayInterface {
    if ( function_exists( 'bookaro_services' ) ) {
        return bookaro_services()->payments();
    }
    $gateway = $GLOBALS['bookaro_payments_gateway'] ?? null;
    if ( $gateway instanceof \Bookaro\Contracts\PaymentsGatewayInterface ) {
        return $gateway;
    }
    return new \Bookaro\Service\NullPaymentsGateway();
}

/**
 * Set the active webhook dispatcher instance.
 */
function bookaro_set_webhook_dispatcher( \Bookaro\Contracts\WebhookDispatcherInterface $dispatcher ): void {
    if ( function_exists( 'bookaro_services' ) ) {
        bookaro_services()->set_webhooks( $dispatcher );
        return;
    }
    $GLOBALS['bookaro_webhook_dispatcher'] = $dispatcher;
}

/**
 * Get the active webhook dispatcher instance.
 */
function bookaro_get_webhook_dispatcher(): \Bookaro\Contracts\WebhookDispatcherInterface {
    if ( function_exists( 'bookaro_services' ) ) {
        return bookaro_services()->webhooks();
    }
    $dispatcher = $GLOBALS['bookaro_webhook_dispatcher'] ?? null;
    if ( $dispatcher instanceof \Bookaro\Contracts\WebhookDispatcherInterface ) {
        return $dispatcher;
    }
    return new \Bookaro\Service\NullWebhookDispatcher();
}

/**
 * Set the active reports provider instance.
 */
function bookaro_set_reports_provider( \Bookaro\Contracts\ReportsProviderInterface $provider ): void {
    if ( function_exists( 'bookaro_services' ) ) {
        bookaro_services()->set_reports( $provider );
        return;
    }
    $GLOBALS['bookaro_reports_provider'] = $provider;
}

/**
 * Get the active reports provider instance.
 */
function bookaro_get_reports_provider(): \Bookaro\Contracts\ReportsProviderInterface {
    if ( function_exists( 'bookaro_services' ) ) {
        return bookaro_services()->reports();
    }
    $provider = $GLOBALS['bookaro_reports_provider'] ?? null;
    if ( $provider instanceof \Bookaro\Contracts\ReportsProviderInterface ) {
        return $provider;
    }
    return new \Bookaro\Service\NullReportsProvider();
}

/**
 * Set the active exports provider instance.
 */
function bookaro_set_exports_provider( \Bookaro\Contracts\ExportsProviderInterface $provider ): void {
    if ( function_exists( 'bookaro_services' ) ) {
        bookaro_services()->set_exports( $provider );
        return;
    }
    $GLOBALS['bookaro_exports_provider'] = $provider;
}

/**
 * Get the active exports provider instance.
 */
function bookaro_get_exports_provider(): \Bookaro\Contracts\ExportsProviderInterface {
    if ( function_exists( 'bookaro_services' ) ) {
        return bookaro_services()->exports();
    }
    $provider = $GLOBALS['bookaro_exports_provider'] ?? null;
    if ( $provider instanceof \Bookaro\Contracts\ExportsProviderInterface ) {
        return $provider;
    }
    return new \Bookaro\Service\NullExportsProvider();
}

/**
 * Check whether payments are enabled.
 */
function bookaro_payments_enabled(): bool {
    $enabled = (bool) bookaro_get_option( 'payments_enabled', 0 );
    $feature = function_exists( 'bookaro_feature_enabled' ) && bookaro_feature_enabled( 'payments' );
    $gateway = function_exists( 'bookaro_get_payments_gateway' )
        ? bookaro_get_payments_gateway()
        : null;
    $available = $gateway instanceof \Bookaro\Contracts\PaymentsGatewayInterface
        ? $gateway->is_available()
        : false;
    return $enabled && $feature && $available;
}

/**
 * Resolve the payments policy setting (Pro).
 */
function bookaro_get_payment_policy(): string {
    $policy = apply_filters( 'bookaro_payment_policy', '' );
    $policy = sanitize_key( (string) $policy );
    return $policy;
}

/**
 * Resolve the default payment choice for customer choice policy.
 */
function bookaro_get_default_payment_choice(): string {
    $choice = apply_filters( 'bookaro_default_payment_choice', '' );
    $choice = sanitize_key( (string) $choice );
    return $choice;
}

/**
 * Enqueue MyListing compatibility styles for Bookaro UI.
 */
add_filter( 'bookaro_block_unpaid_booking_actions', static function ( bool $block, int $booking_id = 0, string $new_status = '' ): bool {
    if ( function_exists( 'bookaro_pro_block_unpaid_booking_actions' ) ) {
        return (bool) bookaro_pro_block_unpaid_booking_actions( $block, $booking_id, $new_status );
    }
    return $block;
}, 10, 3 );

/**
 * Resolve the active integration mode.
 */
function bookaro_is_mylisting_active(): bool {
    return defined( 'MYLISTING_VERSION' )
        || defined( 'MYLISTING_DIR' )
        || class_exists( '\\MyListing\\Plugin' )
        || function_exists( 'mylisting' );
}

/**
 * Normalize a site type value.
 */
function bookaro_normalize_site_type( string $site_type, string $fallback = 'directory' ): string {
    $site_type = sanitize_key( $site_type );
    if ( ! in_array( $site_type, [ 'directory', 'single_business', 'advanced_mapping' ], true ) ) {
        return sanitize_key( $fallback );
    }
    return $site_type;
}

/**
 * Get available site type options.
 */
function bookaro_get_site_type_options(): array {
    $options = [
        'directory' => __( 'Directory lanes (MyListing Directory or Generic Directory)', 'mybookaro' ),
        'single_business' => __( 'Single Business', 'mybookaro' ),
    ];

    $active_site_type = function_exists( 'bookaro_get_site_type_setting' )
        ? sanitize_key( (string) bookaro_get_site_type_setting() )
        : '';
    if ( 'advanced_mapping' === $active_site_type ) {
        // Backward-compatible option for already-configured installs.
        $options['advanced_mapping'] = __( 'Generic Directory (legacy compatibility state active)', 'mybookaro' );
    }

    $filtered = apply_filters( 'bookaro_site_types', $options );
    if ( is_array( $filtered ) ) {
        $options = $filtered;
    }

    if ( ! isset( $options['directory'] ) ) {
        $options = [ 'directory' => __( 'Directory lanes (MyListing Directory or Generic Directory)', 'mybookaro' ) ] + $options;
    }
    if ( ! isset( $options['single_business'] ) ) {
        $options['single_business'] = __( 'Single Business', 'mybookaro' );
    }
    if ( 'advanced_mapping' === $active_site_type && ! isset( $options['advanced_mapping'] ) ) {
        $options['advanced_mapping'] = __( 'Generic Directory (legacy compatibility state active)', 'mybookaro' );
    }

    return $options;
}

/**
 * Get the saved site type setting before runtime fallback.
 */
function bookaro_get_site_type_setting(): string {
    $settings = get_option( 'bookaro_settings', [] );
    $settings = is_array( $settings ) ? $settings : [];

    $site_type = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'site_type', '' )
        : (string) ( $settings['site_type'] ?? '' );
    $site_type = bookaro_normalize_site_type( $site_type, '' );
    if ( '' !== $site_type ) {
        return $site_type;
    }

    // Backward compatibility for installs created before site_type existed.
    $integration_mode = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'integration_mode', 'auto' )
        : (string) ( $settings['integration_mode'] ?? 'auto' );
    $integration_mode = sanitize_key( $integration_mode );
    if ( ! in_array( $integration_mode, [ 'auto', 'mylisting', 'generic' ], true ) ) {
        $integration_mode = 'auto';
    }
    if ( 'generic' === $integration_mode ) {
        $advanced_mapping = function_exists( 'bookaro_get_option' )
            ? ! empty( bookaro_get_option( 'generic_advanced_setup_enabled', 0 ) )
            : ! empty( $settings['generic_advanced_setup_enabled'] );
        if ( $advanced_mapping ) {
            return 'advanced_mapping';
        }
    }

    return 'directory';
}

/**
 * Resolve the active site type.
 */
function bookaro_get_site_type(): string {
    $site_type = bookaro_get_site_type_setting();
    $filtered = apply_filters( 'bookaro_site_type', $site_type );
    return bookaro_normalize_site_type( (string) $filtered, $site_type );
}

/**
 * Check if current site type is single business.
 */
function bookaro_is_single_business_site(): bool {
    return 'single_business' === bookaro_get_site_type();
}

/**
 * Check if current site type is directory.
 */
function bookaro_is_directory_site(): bool {
    return 'directory' === bookaro_get_site_type();
}

/**
 * Check if current site type is advanced mapping.
 */
function bookaro_is_advanced_mapping_site(): bool {
    return 'advanced_mapping' === bookaro_get_site_type();
}

/**
 * Resolve default resource labels by site shape.
 *
 * @return array{singular:string,plural:string}
 */
function bookaro_get_default_resource_labels(): array {
    $site_type = function_exists( 'bookaro_get_site_type' )
        ? sanitize_key( (string) bookaro_get_site_type() )
        : 'directory';

    $labels = [
        'singular' => __( 'Listing', 'mybookaro' ),
        'plural' => __( 'Listings', 'mybookaro' ),
    ];
    if ( 'single_business' === $site_type ) {
        $labels = [
            'singular' => __( 'Business', 'mybookaro' ),
            'plural' => __( 'Services', 'mybookaro' ),
        ];
    }

    $filtered = apply_filters( 'bookaro_default_resource_labels', $labels, $site_type );
    if ( is_array( $filtered ) ) {
        $labels = $filtered;
    }

    $singular = sanitize_text_field( (string) ( $labels['singular'] ?? '' ) );
    $plural = sanitize_text_field( (string) ( $labels['plural'] ?? '' ) );
    if ( '' === $singular ) {
        $singular = ( 'single_business' === $site_type ) ? __( 'Business', 'mybookaro' ) : __( 'Listing', 'mybookaro' );
    }
    if ( '' === $plural ) {
        $plural = ( 'single_business' === $site_type ) ? __( 'Services', 'mybookaro' ) : __( 'Listings', 'mybookaro' );
    }

    return [
        'singular' => $singular,
        'plural' => $plural,
    ];
}

/**
 * Resolve active resource labels from settings with mode-aware defaults.
 *
 * @return array{singular:string,plural:string}
 */
function bookaro_get_resource_labels(): array {
    $defaults = function_exists( 'bookaro_get_default_resource_labels' )
        ? bookaro_get_default_resource_labels()
        : [
            'singular' => __( 'Listing', 'mybookaro' ),
            'plural' => __( 'Listings', 'mybookaro' ),
        ];
    $site_type = function_exists( 'bookaro_get_site_type' )
        ? sanitize_key( (string) bookaro_get_site_type() )
        : 'directory';

    $singular = function_exists( 'bookaro_get_option' )
        ? sanitize_text_field( (string) bookaro_get_option( 'resource_label_singular', '' ) )
        : '';
    $plural = function_exists( 'bookaro_get_option' )
        ? sanitize_text_field( (string) bookaro_get_option( 'resource_label_plural', '' ) )
        : '';

    if ( '' === $singular ) {
        $singular = sanitize_text_field( (string) ( $defaults['singular'] ?? '' ) );
    }
    if ( '' === $plural ) {
        $plural = sanitize_text_field( (string) ( $defaults['plural'] ?? '' ) );
    }

    if ( '' === $singular ) {
        $singular = ( 'single_business' === $site_type ) ? __( 'Business', 'mybookaro' ) : __( 'Listing', 'mybookaro' );
    }
    if ( '' === $plural ) {
        $plural = ( 'single_business' === $site_type ) ? __( 'Services', 'mybookaro' ) : __( 'Listings', 'mybookaro' );
    }

    $labels = [
        'singular' => $singular,
        'plural' => $plural,
    ];

    $filtered = apply_filters( 'bookaro_resource_labels', $labels, $defaults, $site_type );
    if ( is_array( $filtered ) ) {
        $labels = $filtered;
    }

    $labels['singular'] = sanitize_text_field( (string) ( $labels['singular'] ?? '' ) );
    $labels['plural'] = sanitize_text_field( (string) ( $labels['plural'] ?? '' ) );
    if ( '' === $labels['singular'] ) {
        $labels['singular'] = $singular;
    }
    if ( '' === $labels['plural'] ) {
        $labels['plural'] = $plural;
    }

    return $labels;
}

/**
 * Get singular resource label for UI.
 */
function bookaro_get_resource_label_singular(): string {
    $labels = function_exists( 'bookaro_get_resource_labels' )
        ? bookaro_get_resource_labels()
        : [];
    $label = sanitize_text_field( (string) ( $labels['singular'] ?? '' ) );
    if ( '' === $label ) {
        $label = __( 'Listing', 'mybookaro' );
    }
    return $label;
}

/**
 * Get plural resource label for UI.
 */
function bookaro_get_resource_label_plural(): string {
    $labels = function_exists( 'bookaro_get_resource_labels' )
        ? bookaro_get_resource_labels()
        : [];
    $label = sanitize_text_field( (string) ( $labels['plural'] ?? '' ) );
    if ( '' === $label ) {
        $label = __( 'Listings', 'mybookaro' );
    }
    return $label;
}

/**
 * Lowercase helper for UI labels.
 */
function bookaro_resource_label_to_lower( string $label ): string {
    $label = trim( sanitize_text_field( $label ) );
    if ( '' === $label ) {
        return '';
    }
    if ( function_exists( 'mb_strtolower' ) ) {
        $lower = mb_strtolower( $label, 'UTF-8' );
        if ( is_string( $lower ) && '' !== trim( $lower ) ) {
            return trim( $lower );
        }
    }
    return strtolower( $label );
}

/**
 * Get singular resource label in lower-case for sentence text.
 */
function bookaro_get_resource_label_singular_lower(): string {
    $label = function_exists( 'bookaro_get_resource_label_singular' )
        ? bookaro_get_resource_label_singular()
        : __( 'Listing', 'mybookaro' );
    $lower = function_exists( 'bookaro_resource_label_to_lower' )
        ? bookaro_resource_label_to_lower( $label )
        : strtolower( $label );
    return '' !== $lower ? $lower : $label;
}

/**
 * Get plural resource label in lower-case for sentence text.
 */
function bookaro_get_resource_label_plural_lower(): string {
    $label = function_exists( 'bookaro_get_resource_label_plural' )
        ? bookaro_get_resource_label_plural()
        : __( 'Listings', 'mybookaro' );
    $lower = function_exists( 'bookaro_resource_label_to_lower' )
        ? bookaro_resource_label_to_lower( $label )
        : strtolower( $label );
    return '' !== $lower ? $lower : $label;
}

function bookaro_get_integration_mode_options(): array {
    $options = [
        'auto' => __( 'Auto-detect directory lane (MyListing Directory when active)', 'mybookaro' ),
        'mylisting' => __( 'MyListing Directory', 'mybookaro' ),
        'generic' => __( 'Generic Directory', 'mybookaro' ),
    ];

    $filtered = apply_filters( 'bookaro_integration_modes', $options );
    if ( is_array( $filtered ) ) {
        $options = $filtered;
    }

    $generic_enabled = ! ( defined( 'BOOKARO_ENABLE_GENERIC_MODE' ) && ! BOOKARO_ENABLE_GENERIC_MODE );
    if ( ! $generic_enabled ) {
        unset( $options['generic'] );
    } elseif ( ! isset( $options['generic'] ) ) {
        $options['generic'] = __( 'Generic Directory', 'mybookaro' );
    }

    if ( ! isset( $options['auto'] ) ) {
        $options = [ 'auto' => __( 'Auto-detect directory lane (MyListing Directory when active)', 'mybookaro' ) ] + $options;
    }

    return $options;
}

/**
 * Get the saved integration mode setting before runtime fallback.
 */
function bookaro_get_integration_mode_setting(): string {
    $mode = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'integration_mode', 'auto' )
        : 'auto';
    $mode = sanitize_key( $mode );
    if ( ! in_array( $mode, [ 'auto', 'mylisting', 'generic' ], true ) ) {
        $mode = 'auto';
    }
    return $mode;
}

/**
 * Read plugin settings as an array.
 *
 * @return array<string,mixed>
 */
function bookaro_get_settings_snapshot(): array {
    $settings = get_option( 'bookaro_settings', [] );
    return is_array( $settings ) ? $settings : [];
}

/**
 * Determine whether Generic setup has explicit admin confirmation metadata.
 *
 * @param array<string,mixed> $settings Optional settings snapshot.
 */
function bookaro_is_generic_setup_explicitly_confirmed( array $settings = [] ): bool {
    if ( empty( $settings ) ) {
        $settings = bookaro_get_settings_snapshot();
    }

    if ( ! empty( $settings['generic_setup_confirmed'] ) ) {
        return true;
    }

    if ( absint( $settings['generic_setup_confirmed_at'] ?? 0 ) > 0 ) {
        return true;
    }

    $confirmation_source = sanitize_key( (string) ( $settings['generic_setup_confirmation_source'] ?? '' ) );
    if ( '' !== $confirmation_source ) {
        return true;
    }

    // Explicit Generic mode selection is an admin-confirmed path.
    $integration_mode = sanitize_key( (string) ( $settings['integration_mode'] ?? '' ) );
    if ( 'generic' === $integration_mode ) {
        return true;
    }

    return false;
}

/**
 * Determine whether Generic setup has been confirmed.
 *
 * @param array<string,mixed> $settings Optional settings snapshot.
 */
function bookaro_is_generic_setup_confirmed( array $settings = [] ): bool {
    if ( empty( $settings ) ) {
        $settings = bookaro_get_settings_snapshot();
    }

    if ( bookaro_is_generic_setup_explicitly_confirmed( $settings ) ) {
        return true;
    }

    if ( ! empty( $settings['generic_setup_completed'] ) ) {
        return true;
    }

    $confirmation_string_markers = [
        'generic_validation_review_hash',
    ];
    foreach ( $confirmation_string_markers as $key ) {
        $value = trim( sanitize_text_field( (string) ( $settings[ $key ] ?? '' ) ) );
        if ( '' !== $value ) {
            return true;
        }
    }

    $confirmation_int_markers = [
        'generic_validation_reviewed_at',
        'generic_setup_listing_id',
        'generic_setup_provider_user_id',
        'generic_setup_service_id',
        'generic_setup_booking_page_id',
        'generic_setup_provider_dashboard_page_id',
        'generic_setup_customer_dashboard_page_id',
        'generic_setup_services_page_id',
    ];
    foreach ( $confirmation_int_markers as $key ) {
        if ( absint( $settings[ $key ] ?? 0 ) > 0 ) {
            return true;
        }
    }

    if ( ! empty( $settings['listing_post_type_pages'] ) && is_array( $settings['listing_post_type_pages'] ) ) {
        foreach ( $settings['listing_post_type_pages'] as $post_type => $contexts ) {
            $post_type = sanitize_key( (string) $post_type );
            if ( '' === $post_type || ! is_array( $contexts ) ) {
                continue;
            }
            foreach ( [ 'booking', 'provider_dashboard', 'customer_dashboard', 'services' ] as $context ) {
                if ( absint( $contexts[ $context ] ?? 0 ) > 0 ) {
                    return true;
                }
            }
        }
    }

    // Backward compatibility: legacy configured Generic installs that rely on
    // existing provider/service content should continue to be treated as
    // confirmed even if no explicit confirmation key exists yet.
    return bookaro_has_legacy_generic_content_markers();
}

/**
 * Detect whether Generic setup appears intentionally configured.
 *
 * @param array<string,mixed> $settings Optional settings snapshot.
 */
function bookaro_has_generic_setup_markers( array $settings = [] ): bool {
    if ( empty( $settings ) ) {
        $settings = bookaro_get_settings_snapshot();
    }
    if ( empty( $settings ) ) {
        return false;
    }

    $integration_mode = sanitize_key( (string) ( $settings['integration_mode'] ?? '' ) );
    if ( 'generic' === $integration_mode ) {
        return true;
    }

    $truthy_markers = [
        'generic_setup_confirmed',
        'generic_setup_completed',
        'generic_advanced_setup_enabled',
        'provider_services_enabled',
        'allow_provider_custom_services',
        'allow_provider_override_catalog',
        'auto_insert_booking_button_enabled',
    ];
    foreach ( $truthy_markers as $key ) {
        if ( ! empty( $settings[ $key ] ) ) {
            return true;
        }
    }

    $string_markers = [
        'generic_setup_preset',
        'provider_resolution_mode',
        'provider_meta_key',
        'listing_provider_meta_key',
        'services_model',
        'generic_services_mode',
        'auto_insert_booking_button',
        'auto_insert_booking_button_selector',
        'generic_validation_review_hash',
        'generic_setup_confirmation_source',
    ];
    foreach ( $string_markers as $key ) {
        if ( ! array_key_exists( $key, $settings ) ) {
            continue;
        }
        if ( '' !== trim( sanitize_text_field( (string) $settings[ $key ] ) ) ) {
            return true;
        }
    }

    $int_markers = [
        'generic_validation_reviewed_at',
        'generic_setup_confirmed_at',
        'generic_setup_listing_id',
        'generic_setup_provider_user_id',
        'generic_setup_service_id',
        'generic_setup_booking_page_id',
        'generic_setup_provider_dashboard_page_id',
        'generic_setup_customer_dashboard_page_id',
        'generic_setup_services_page_id',
        'booking_page_id',
        'provider_dashboard_page_id',
        'customer_dashboard_page_id',
        'default_provider_id',
    ];
    foreach ( $int_markers as $key ) {
        if ( absint( $settings[ $key ] ?? 0 ) > 0 ) {
            return true;
        }
    }

    if ( ! empty( $settings['allowed_listing_post_types'] ) && is_array( $settings['allowed_listing_post_types'] ) ) {
        return true;
    }
    if ( ! empty( $settings['allowed_listing_pages'] ) && is_array( $settings['allowed_listing_pages'] ) ) {
        return true;
    }
    if ( ! empty( $settings['listing_post_type_pages'] ) && is_array( $settings['listing_post_type_pages'] ) ) {
        return true;
    }

    return false;
}

/**
 * Detect legacy Generic usage markers for sites that stayed on Auto mode.
 */
function bookaro_has_legacy_generic_content_markers(): bool {
    static $has_markers = null;
    if ( null !== $has_markers ) {
        return $has_markers;
    }

    $has_markers = false;
    $post_types = [];
    if ( defined( 'BOOKARO_PROVIDER_POST_TYPE' ) ) {
        $post_types[] = sanitize_key( (string) BOOKARO_PROVIDER_POST_TYPE );
    }
    if ( defined( 'BOOKARO_SERVICE_POST_TYPE' ) ) {
        $post_types[] = sanitize_key( (string) BOOKARO_SERVICE_POST_TYPE );
    }
    $post_types = array_values( array_unique( array_filter( $post_types ) ) );

    foreach ( $post_types as $post_type ) {
        $counts = wp_count_posts( $post_type );
        if ( ! is_object( $counts ) ) {
            continue;
        }
        foreach ( [ 'publish', 'private', 'draft', 'pending', 'future' ] as $status ) {
            if ( (int) ( $counts->{$status} ?? 0 ) > 0 ) {
                $has_markers = true;
                break 2;
            }
        }
    }

    return $has_markers;
}

/**
 * Determine whether Auto mode is unresolved on non-MyListing sites.
 */
function bookaro_is_generic_auto_unresolved(): bool {
    if ( defined( 'BOOKARO_ENABLE_GENERIC_MODE' ) && ! BOOKARO_ENABLE_GENERIC_MODE ) {
        return false;
    }

    if ( 'auto' !== bookaro_get_integration_mode_setting() ) {
        return false;
    }

    if ( bookaro_is_mylisting_active() ) {
        return false;
    }

    $settings = bookaro_get_settings_snapshot();
    $generic_confirmed = bookaro_is_generic_setup_confirmed( $settings );
    $generic_confirmed = (bool) apply_filters( 'bookaro_generic_setup_confirmed', $generic_confirmed, $settings );

    return ! $generic_confirmed;
}

/**
 * Determine whether Generic requires explicit confirmation.
 */
function bookaro_generic_requires_confirmation(): bool {
    return bookaro_is_generic_auto_unresolved();
}

/**
 * Determine whether Generic is running via legacy fallback markers.
 */
function bookaro_is_generic_legacy_fallback_active(): bool {
    if ( defined( 'BOOKARO_ENABLE_GENERIC_MODE' ) && ! BOOKARO_ENABLE_GENERIC_MODE ) {
        return false;
    }

    if ( 'auto' !== bookaro_get_integration_mode_setting() ) {
        return false;
    }

    if ( bookaro_is_mylisting_active() ) {
        return false;
    }

    $settings = bookaro_get_settings_snapshot();
    $generic_confirmed = bookaro_is_generic_setup_confirmed( $settings );
    $generic_confirmed = (bool) apply_filters( 'bookaro_generic_setup_confirmed', $generic_confirmed, $settings );
    if ( ! $generic_confirmed ) {
        return false;
    }

    $explicitly_confirmed = bookaro_is_generic_setup_explicitly_confirmed( $settings );
    $explicitly_confirmed = (bool) apply_filters( 'bookaro_generic_setup_explicitly_confirmed', $explicitly_confirmed, $settings );

    $legacy_fallback_active = ! $explicitly_confirmed;

    return (bool) apply_filters( 'bookaro_generic_legacy_fallback_active', $legacy_fallback_active, $settings );
}

function bookaro_get_integration_mode(): string {
    $mode = bookaro_get_integration_mode_setting();
    $site_type = function_exists( 'bookaro_get_site_type' )
        ? bookaro_get_site_type()
        : 'directory';

    if ( in_array( $site_type, [ 'single_business', 'advanced_mapping' ], true ) ) {
        $mode = 'generic';
    } else {
        $mylisting_detected = bookaro_is_mylisting_active();
        if ( 'auto' === $mode ) {
            // Keep runtime fallback for backward compatibility. Auto mode still
            // resolves to Generic when MyListing is absent, while unresolved
            // confirmation state is surfaced separately via admin checks/notices.
            $mode = $mylisting_detected ? 'mylisting' : 'generic';
        }
    }

    $filtered = apply_filters( 'bookaro_integration_mode', $mode );
    $filtered = sanitize_key( (string) $filtered );
    if ( 'auto' === $filtered ) {
        $filtered = $mode;
    }
    if ( ! in_array( $filtered, [ 'mylisting', 'generic' ], true ) ) {
        $filtered = $mode;
    }
    if ( in_array( $site_type, [ 'single_business', 'advanced_mapping' ], true ) ) {
        $filtered = 'generic';
    }
    return $filtered;
}

/**
 * Normalize booking UI placement strategy.
 */
function bookaro_normalize_booking_placement_strategy( string $strategy, string $fallback = 'directory_template' ): string {
    $strategy = sanitize_key( $strategy );
    if ( ! in_array( $strategy, [ 'directory_template', 'normal_page', 'shortcode_only', 'css_selector' ], true ) ) {
        $strategy = sanitize_key( $fallback );
    }
    if ( ! in_array( $strategy, [ 'directory_template', 'normal_page', 'shortcode_only', 'css_selector' ], true ) ) {
        $strategy = 'directory_template';
    }
    return $strategy;
}

/**
 * Resolve booking placement strategy for Generic mode.
 */
function bookaro_get_booking_placement_strategy(): string {
    $settings = get_option( 'bookaro_settings', [] );
    $site_type = function_exists( 'bookaro_get_site_type' )
        ? sanitize_key( (string) bookaro_get_site_type() )
        : 'directory';
    $default_strategy = ( 'single_business' === $site_type )
        ? 'normal_page'
        : 'directory_template';

    $strategy = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'booking_placement_strategy', '' )
        : (string) ( is_array( $settings ) ? ( $settings['booking_placement_strategy'] ?? '' ) : '' );
    $strategy = bookaro_normalize_booking_placement_strategy( $strategy, '' );
    if ( '' !== $strategy ) {
        $filtered = apply_filters( 'bookaro_booking_placement_strategy', $strategy, $settings );
        $resolved = bookaro_normalize_booking_placement_strategy( (string) $filtered, $strategy );
        // Single Business has no listings, so directory_template can never resolve a
        // booking target. Coerce it to normal_page so the front-end and the launch
        // validation agree — otherwise a stored directory_template leaves the
        // "Booking entry placement" launch check failing with no fixable UI control.
        if ( 'single_business' === $site_type && 'directory_template' === $resolved ) {
            $resolved = 'normal_page';
        }
        return $resolved;
    }

    $enabled = null;
    if ( is_array( $settings ) && array_key_exists( 'auto_insert_booking_button_enabled', $settings ) ) {
        $enabled = ! empty( $settings['auto_insert_booking_button_enabled'] );
    }

    $mode = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'auto_insert_booking_button', '' )
        : '';
    if ( '' === $mode && is_array( $settings ) ) {
        $mode = (string) ( $settings['auto_insert_booking_button'] ?? '' );
    }
    $mode = sanitize_key( $mode );
    if ( null === $enabled ) {
        $enabled = ( '' !== $mode );
    }

    if ( ! $enabled ) {
        $strategy = 'shortcode_only';
    } elseif ( 'selector' === $mode ) {
        $strategy = 'css_selector';
    } else {
        $strategy = $default_strategy;
    }

    $strategy = bookaro_normalize_booking_placement_strategy( $strategy, $default_strategy );
    $filtered = apply_filters( 'bookaro_booking_placement_strategy', $strategy, $settings );
    $resolved = bookaro_normalize_booking_placement_strategy( (string) $filtered, $strategy );
    if ( 'single_business' === $site_type && 'directory_template' === $resolved ) {
        $resolved = 'normal_page';
    }
    return $resolved;
}

/**
 * Read the Generic validation review snapshot (config hash + timestamp of the
 * last passing System Status run).
 *
 * Stored in its own option rather than inside bookaro_settings: that array is
 * large and could not be reliably re-written from the admin render path on some
 * MySQL configs, which left the "settings changed, re-run validation" reminder
 * permanently stuck. Falls back to the legacy in-settings keys for sites saved
 * before the snapshot was split out.
 */
function bookaro_get_generic_validation_snapshot(): array {
    $snap = get_option( 'bookaro_generic_validation_snapshot', null );
    if ( is_array( $snap ) && array_key_exists( 'hash', $snap ) ) {
        return [
            'hash' => (string) $snap['hash'],
            'at'   => absint( $snap['at'] ?? 0 ),
        ];
    }
    $settings = get_option( 'bookaro_settings', [] );
    if ( ! is_array( $settings ) ) {
        $settings = [];
    }
    return [
        'hash' => (string) ( $settings['generic_validation_review_hash'] ?? '' ),
        'at'   => absint( $settings['generic_validation_reviewed_at'] ?? 0 ),
    ];
}

/**
 * Persist the Generic validation review snapshot to its dedicated option.
 */
function bookaro_set_generic_validation_snapshot( string $hash, int $at = 0 ): bool {
    $at = $at > 0 ? $at : time();
    return (bool) update_option(
        'bookaro_generic_validation_snapshot',
        [ 'hash' => $hash, 'at' => $at ],
        false
    );
}

/**
 * Canonical hash of the Generic-mode configuration the launch-validation snapshot
 * tracks. Shared by every admin surface (System Status, Getting Started, Setup
 * Wizard, Settings) so a snapshot recorded on one page matches what the others
 * compute. Previously each controller kept its own divergent copy, so once a
 * snapshot was recorded the "settings changed, re-run validation" reminder could
 * never clear on the pages whose hash differed.
 */
function bookaro_build_generic_validation_config_hash( array $opts ): string {
    $allowed_types = $opts['allowed_listing_post_types'] ?? [];
    if ( ! is_array( $allowed_types ) ) {
        $allowed_types = [ $allowed_types ];
    }
    $allowed_types = array_values( array_unique( array_filter( array_map( 'sanitize_key', $allowed_types ) ) ) );
    sort( $allowed_types );

    $allowed_pages = $opts['allowed_listing_pages'] ?? [];
    if ( ! is_array( $allowed_pages ) ) {
        $allowed_pages = [ $allowed_pages ];
    }
    $allowed_pages = array_values( array_unique( array_filter( array_map( 'absint', $allowed_pages ) ) ) );
    sort( $allowed_pages );

    $listing_page_overrides = [];
    $raw_overrides = $opts['listing_post_type_pages'] ?? [];
    if ( is_array( $raw_overrides ) ) {
        foreach ( $raw_overrides as $post_type => $contexts ) {
            $post_type = sanitize_key( (string) $post_type );
            if ( '' === $post_type || ! is_array( $contexts ) ) {
                continue;
            }
            $row = [];
            foreach ( [ 'booking', 'provider_dashboard', 'customer_dashboard', 'services' ] as $context ) {
                $page_id = absint( $contexts[ $context ] ?? 0 );
                if ( $page_id > 0 ) {
                    $row[ $context ] = $page_id;
                }
            }
            if ( ! empty( $row ) ) {
                $listing_page_overrides[ $post_type ] = $row;
            }
        }
    }
    ksort( $listing_page_overrides );

    $site_type = sanitize_key( (string) ( $opts['site_type'] ?? 'directory' ) );
    if ( ! in_array( $site_type, [ 'directory', 'single_business', 'advanced_mapping' ], true ) ) {
        $site_type = 'directory';
    }

    $payload = [
        'site_type' => $site_type,
        'integration_mode_setting' => sanitize_key( (string) ( $opts['integration_mode'] ?? 'auto' ) ),
        'integration_mode_resolved' => function_exists( 'bookaro_get_integration_mode' )
            ? sanitize_key( (string) bookaro_get_integration_mode() )
            : sanitize_key( (string) ( $opts['integration_mode'] ?? 'auto' ) ),
        'booking_placement_strategy' => function_exists( 'bookaro_get_booking_placement_strategy' )
            ? sanitize_key( (string) bookaro_get_booking_placement_strategy() )
            : sanitize_key( (string) ( $opts['booking_placement_strategy'] ?? '' ) ),
        'ui_mode' => sanitize_key( (string) ( $opts['ui_mode'] ?? '' ) ),
        'generic_advanced_setup_enabled' => ! empty( $opts['generic_advanced_setup_enabled'] ) ? 1 : 0,
        'generic_setup_preset' => sanitize_key( (string) ( $opts['generic_setup_preset'] ?? '' ) ),
        'allowed_listing_post_types' => $allowed_types,
        'allowed_listing_pages' => $allowed_pages,
        'listing_post_type' => sanitize_key( (string) ( $opts['listing_post_type'] ?? '' ) ),
        'provider_resolution_mode' => sanitize_key( (string) ( $opts['provider_resolution_mode'] ?? '' ) ),
        'provider_meta_key' => sanitize_key( (string) ( $opts['provider_meta_key'] ?? $opts['listing_provider_meta_key'] ?? '' ) ),
        'services_model' => sanitize_key( (string) ( $opts['services_model'] ?? '' ) ),
        'generic_services_mode' => sanitize_key( (string) ( $opts['generic_services_mode'] ?? '' ) ),
        'provider_services_enabled' => ! empty( $opts['provider_services_enabled'] ) ? 1 : 0,
        'allow_provider_custom_services' => ! empty( $opts['allow_provider_custom_services'] ) ? 1 : 0,
        'allow_provider_override_catalog' => ! empty( $opts['allow_provider_override_catalog'] ) ? 1 : 0,
        'auto_insert_booking_button_enabled' => ! empty( $opts['auto_insert_booking_button_enabled'] ) ? 1 : 0,
        'auto_insert_booking_button' => sanitize_key( (string) ( $opts['auto_insert_booking_button'] ?? '' ) ),
        'auto_insert_booking_button_selector' => sanitize_text_field( (string) ( $opts['auto_insert_booking_button_selector'] ?? '' ) ),
        'generic_setup_pages' => [
            'booking' => absint( $opts['generic_setup_booking_page_id'] ?? 0 ),
            'provider_dashboard' => absint( $opts['generic_setup_provider_dashboard_page_id'] ?? 0 ),
            'customer_dashboard' => absint( $opts['generic_setup_customer_dashboard_page_id'] ?? 0 ),
            'services' => absint( $opts['generic_setup_services_page_id'] ?? 0 ),
        ],
        'global_pages' => [
            'booking' => absint( $opts['booking_page_id'] ?? 0 ),
            'provider_dashboard' => absint( $opts['provider_dashboard_page_id'] ?? 0 ),
            'customer_dashboard' => absint( $opts['customer_dashboard_page_id'] ?? 0 ),
        ],
        'generic_setup_context' => [
            'listing_id' => absint( $opts['generic_setup_listing_id'] ?? 0 ),
            'provider_user_id' => absint( $opts['generic_setup_provider_user_id'] ?? 0 ),
            'service_id' => absint( $opts['generic_setup_service_id'] ?? 0 ),
            'default_provider_id' => absint( $opts['default_provider_id'] ?? 0 ),
        ],
        'listing_post_type_pages' => $listing_page_overrides,
    ];

    $json = wp_json_encode( $payload );
    if ( ! is_string( $json ) || '' === $json ) {
        return '';
    }
    return hash( 'sha256', $json );
}

/**
 * Resolve the auto-insert booking button placement (Generic mode).
 */
function bookaro_get_auto_insert_booking_button_mode(): string {
    $settings = get_option( 'bookaro_settings', [] );
    $strategy = function_exists( 'bookaro_get_booking_placement_strategy' )
        ? bookaro_get_booking_placement_strategy()
        : 'directory_template';
    $strategy = bookaro_normalize_booking_placement_strategy( (string) $strategy, 'directory_template' );

    if ( 'shortcode_only' === $strategy ) {
        return '';
    }

    $enabled = null;
    if ( is_array( $settings ) && array_key_exists( 'auto_insert_booking_button_enabled', $settings ) ) {
        $enabled = ! empty( $settings['auto_insert_booking_button_enabled'] );
    }

    $mode = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'auto_insert_booking_button', '' )
        : '';
    $mode = sanitize_key( $mode );
    if ( 'css_selector' === $strategy ) {
        $mode = 'selector';
    } else {
        if ( ! in_array( $mode, [ 'before_content', 'after_content', 'selector' ], true ) ) {
            $mode = 'after_content';
        }
        if ( 'selector' === $mode ) {
            $mode = 'after_content';
        }
    }
    if ( null === $enabled ) {
        $enabled = ( $mode !== '' );
    }
    if ( ! $enabled ) {
        return '';
    }
    return $mode;
}

/**
 * Resolve the auto-insert booking button CSS selector.
 */
function bookaro_get_auto_insert_booking_button_selector(): string {
    $selector = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'auto_insert_booking_button_selector', '' )
        : '';
    return trim( sanitize_text_field( $selector ) );
}

/**
 * Resolve the base account URL (My Account page in MyListing/WooCommerce setups).
 */
function bookaro_get_account_url(): string {
    $default = site_url( '/my-account/' );
    $url = (string) apply_filters( 'bookaro_account_url', $default );
    if ( '' === $url ) {
        $url = $default;
    }
    return trailingslashit( $url );
}

/**
 * Check whether WooCommerce is active.
 */
function bookaro_is_woocommerce_active(): bool {
    return class_exists( 'WooCommerce' ) || defined( 'WC_VERSION' ) || function_exists( 'WC' );
}

/**
 * Resolve the UI mode, falling back to shortcodes if WooCommerce is unavailable.
 */
function bookaro_get_ui_mode(): string {
    $integration_mode = function_exists( 'bookaro_get_integration_mode' )
        ? (string) bookaro_get_integration_mode()
        : 'auto';
    $integration_mode = sanitize_key( $integration_mode );
    $default_ui_mode = ( 'generic' === $integration_mode ) ? 'shortcodes' : 'woocommerce';

    $ui_mode = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'ui_mode', $default_ui_mode )
        : $default_ui_mode;
    if ( ! in_array( $ui_mode, [ 'woocommerce', 'shortcodes' ], true ) ) {
        $ui_mode = $default_ui_mode;
    }
    if ( 'woocommerce' === $ui_mode && ! bookaro_is_woocommerce_active() ) {
        $ui_mode = 'shortcodes';
    }
    return $ui_mode;
}

/**
 * Resolve the dashboard URL for provider/customer views.
 *
 * @param array $args Optional query args. Supports listing_id, post_type, context.
 */
function bookaro_get_dashboard_url( array $args = [] ): string {
    $ui_mode = bookaro_get_ui_mode();

    $listing_id = absint( $args['listing_id'] ?? 0 );
    $post_type = '';
    if ( $listing_id > 0 ) {
        $post_type = get_post_type( $listing_id );
    }
    if ( $post_type === '' && ! empty( $args['post_type'] ) ) {
        $post_type = sanitize_key( (string) $args['post_type'] );
    }
    if ( $post_type === '' && function_exists( 'get_the_ID' ) ) {
        $current_id = (int) get_the_ID();
        if ( $current_id > 0 ) {
            $post_type = get_post_type( $current_id );
        }
    }

    $context = sanitize_key( (string) ( $args['context'] ?? '' ) );
    if ( 'provider' === $context ) {
        $context = 'provider_dashboard';
    } elseif ( in_array( $context, [ 'customer', 'owner' ], true ) ) {
        $context = 'customer_dashboard';
    }

    $base = '';
    if ( 'shortcodes' === $ui_mode && $post_type !== '' ) {
        if ( '' === $context ) {
            if ( function_exists( 'bookaro_user_has_provider_access' ) && bookaro_user_has_provider_access() ) {
                $context = 'provider_dashboard';
            } elseif ( function_exists( 'bookaro_user_has_customer_access' ) && bookaro_user_has_customer_access() ) {
                $context = 'customer_dashboard';
            }
        }
        if ( $context !== '' && function_exists( 'bookaro_get_page_for_context' ) ) {
            $mode = function_exists( 'bookaro_get_integration_mode' ) ? bookaro_get_integration_mode() : 'auto';
            $page_id = bookaro_get_page_for_context( $mode, $post_type, $context );
            if ( $page_id > 0 ) {
                $base = get_permalink( $page_id );
            }
        }
    }

    // Bug 52: trailingslashit() is fine for pretty permalinks ("/dashboard/")
    // but corrupts query-string permalinks ("?page_id=31" -> "?page_id=31/").
    // When add_query_arg() later appends tab=bookings the result is
    // "?page_id=31/&tab=bookings", which PHP parses as page_id="31/" and the
    // "Manage booking" CTA in notification emails opens the wrong page.
    // Use the helper below which only slashes path-style URLs.
    $maybe_slash = static function ( string $u ): string {
        if ( '' === $u ) {
            return $u;
        }
        // Strip query+fragment, then trailingslashit only the path. Recombine.
        $parts = wp_parse_url( $u );
        if ( ! is_array( $parts ) || empty( $parts['path'] ) ) {
            // No path -> nothing to slash; return as-is.
            return $u;
        }
        $parts['path'] = trailingslashit( $parts['path'] );
        $rebuilt = ( isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '' )
            . ( $parts['host'] ?? '' )
            . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' )
            . $parts['path']
            . ( isset( $parts['query'] ) ? '?' . $parts['query'] : '' )
            . ( isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '' );
        return $rebuilt;
    };

    if ( $base !== '' ) {
        $url = $maybe_slash( $base );
    } else {
        // No per-post-type page resolved. Try the admin-configured customer
        // dashboard page (Settings > General > Customer Dashboard Page, or the
        // wizard's "Customer Dashboard Page" field). Both store IDs in the
        // options blob, keyed slightly differently per setup preset.
        $opts                = get_option( 'bookaro_settings', [] );
        $configured_page_id  = absint( $opts['customer_dashboard_page_id'] ?? 0 );
        if ( $configured_page_id < 1 ) {
            $configured_page_id = absint( $opts['generic_setup_customer_dashboard_page_id'] ?? 0 );
        }
        $configured_url = $configured_page_id > 0 ? (string) get_permalink( $configured_page_id ) : '';

        if ( '' !== $configured_url ) {
            $url = $maybe_slash( $configured_url );
        } else {
            $integration_mode = function_exists( 'bookaro_get_integration_mode' )
                ? sanitize_key( (string) bookaro_get_integration_mode() )
                : 'auto';
            if ( 'mylisting' === $integration_mode ) {
                // MyListing theme registers /my-account/bookings-and-management/
                // as an account-section endpoint; safe to link to it there.
                $base = bookaro_get_account_url();
                $url  = trailingslashit( $base ) . 'bookings-and-management/';
            } else {
                // Single Business / Generic Directory with no dashboard page
                // configured. Falling back to the MyListing URL would 404
                // here (verified visually). Use the site home as the
                // best-effort destination so the "Manage booking" link at
                // least lands somewhere real instead of a 404.
                $url = home_url( '/' );
            }
        }
    }

    $query_args = $args;
    unset( $query_args['context'], $query_args['listing_id'], $query_args['post_type'] );
    if ( ! empty( $query_args ) ) {
        $url = add_query_arg( $query_args, $url );
    }
    $url = (string) apply_filters( 'bookaro_dashboard_url', $url, $args );
    return $url;
}

/**
 * Resolve the login URL for booking flows.
 *
 * Falls back to a different base depending on integration mode:
 *   - MyListing: `/my-account/` (the MyListing theme registers this).
 *   - Anything else: WordPress core's `wp_login_url()` (`/wp-login.php`),
 *     which always exists.
 *
 * Without the mode-aware fallback, Single Business / Generic Directory
 * installs would send guests clicking "Login or sign up" to a 404 page —
 * verified visually on bookaro-test before the fix.
 */
function bookaro_get_login_url( string $redirect = '' ): string {
    $integration_mode = function_exists( 'bookaro_get_integration_mode' )
        ? sanitize_key( (string) bookaro_get_integration_mode() )
        : 'auto';

    if ( 'mylisting' === $integration_mode ) {
        $base = bookaro_get_account_url();
        $url  = '' !== $redirect ? add_query_arg( 'redirect_to', $redirect, $base ) : $base;
    } else {
        $base = function_exists( 'wp_login_url' ) ? wp_login_url() : bookaro_get_account_url();
        $url  = '' !== $redirect && function_exists( 'wp_login_url' )
            ? wp_login_url( $redirect )
            : $base;
    }

    $url = (string) apply_filters( 'bookaro_login_url', $url, $redirect );
    return $url;
}

/**
 * Show an admin notice when payments are unavailable.
 */
function bookaro_payments_unavailable_notice(): void {
    if ( ! current_user_can( 'activate_plugins' ) ) {
        return;
    }

    $message = __( 'MyBookaro payments are unavailable. Payments have been disabled. Please ensure MyBookaro Pro and a supported payments integration are active.', 'mybookaro' );

    echo '<div class="notice notice-error"><p>' . wp_kses_post( $message ) . '</p></div>';
}

/**
 * Determine whether the current request is within MyBookaro admin screens.
 */
function bookaro_is_bookaro_admin_context(): bool {
    if ( ! is_admin() ) {
        return false;
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( (string) $_GET['page'] ) ) : '';
    if ( '' !== $page && 0 === strpos( $page, 'bookaro-' ) ) {
        return true;
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( (string) $_GET['post_type'] ) ) : '';
    if ( '' !== $post_type ) {
        if ( defined( 'BOOKARO_PROVIDER_POST_TYPE' ) && $post_type === sanitize_key( (string) BOOKARO_PROVIDER_POST_TYPE ) ) {
            return true;
        }
        if ( defined( 'BOOKARO_SERVICE_POST_TYPE' ) && $post_type === sanitize_key( (string) BOOKARO_SERVICE_POST_TYPE ) ) {
            return true;
        }
    }

    return false;
}

/**
 * Show a setup-required notice when Auto mode is unresolved.
 */
function bookaro_generic_auto_unresolved_notice(): void {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    if ( ! function_exists( 'bookaro_is_generic_auto_unresolved' ) || ! bookaro_is_generic_auto_unresolved() ) {
        return;
    }

    // Setup Wizard/System Status already show richer state-aware guidance.
    if ( function_exists( 'bookaro_is_bookaro_admin_context' ) && bookaro_is_bookaro_admin_context() ) {
        return;
    }

    $message = __( 'MyListing is not detected and Auto-detect lane routing is unresolved. Confirm Generic Directory lane setup in Setup Wizard before launch.', 'mybookaro' );
    $action_label = __( 'Confirm Generic Setup', 'mybookaro' );
    $action_url = admin_url( 'admin.php?page=bookaro-setup-wizard&step=modes' );

    echo '<div class="notice notice-warning"><p>'
        . esc_html( $message )
        . ' <a href="' . esc_url( $action_url ) . '">' . esc_html( $action_label ) . '</a>'
        . '</p></div>';
}

/**
 * Show a review notice when Generic is running via legacy fallback markers.
 */
function bookaro_generic_legacy_fallback_notice(): void {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    if ( ! function_exists( 'bookaro_is_generic_legacy_fallback_active' ) || ! bookaro_is_generic_legacy_fallback_active() ) {
        return;
    }

    // Setup Wizard/System Status already show richer state-aware guidance.
    if ( function_exists( 'bookaro_is_bookaro_admin_context' ) && bookaro_is_bookaro_admin_context() ) {
        return;
    }

    $message = __( 'MyListing is not detected. Generic Directory lane is active through legacy Auto fallback markers, but explicit lane confirmation metadata is missing. Review Setup Wizard > Lane and save once before launch.', 'mybookaro' );
    $action_label = __( 'Review Generic Confirmation', 'mybookaro' );
    $action_url = admin_url( 'admin.php?page=bookaro-setup-wizard&step=modes' );

    echo '<div class="notice notice-warning"><p>'
        . esc_html( $message )
        . ' <a href="' . esc_url( $action_url ) . '">' . esc_html( $action_label ) . '</a>'
        . '</p></div>';
}

/**
 * Helper to get a single option value from the plugin settings.
 *
 * @param string $key     Option key.
 * @param mixed  $default Default if not set.
 * @return mixed
 */
function bookaro_get_option( string $key, $default = '' ) {
    // Bug 53: 'require_login' used to be hardcoded `return 1;` here, which
    // silently overrode the toggle in Settings > Booking. Admins could flip
    // "Require login to book" off in the UI, see `require_login => 0` in
    // bookaro_settings, and STILL get 401 bookaro_auth_required on every
    // anonymous booking attempt — the modal would show the form, the
    // visitor would fill it in, click Confirm, and only then meet the
    // server's rejection. The hardcoded `return 1` was undocumented and
    // there is no comment / feature flag justifying it; removing it lets
    // the option flow through OptionRepository like every other setting,
    // and the second argument to bookaro_get_option still defaults to 1
    // at every call site (search confirms) so the safe-by-default behavior
    // (login required) is preserved when the option is genuinely unset.
    $repo = new \Bookaro\Repository\OptionRepository();
    return $repo->get( $key, $default );
}

/**
 * Resolve capability used for MyBookaro admin menus.
 */
function bookaro_get_admin_menu_capability(): string {
    $default = 'manage_options';
    if ( current_user_can( 'manage_options' ) ) {
        $default = 'manage_options';
    } elseif ( current_user_can( 'activate_plugins' ) ) {
        $default = 'activate_plugins';
    }

    $capability = apply_filters( 'bookaro_admin_menu_capability', $default );
    $capability = is_string( $capability ) ? sanitize_key( $capability ) : '';
    if ( '' === $capability ) {
        return $default;
    }
    return $capability;
}

/**
 * Check whether current user can access MyBookaro admin management screens.
 */
function bookaro_current_user_can_manage_admin(): bool {
    $capability = bookaro_get_admin_menu_capability();
    if ( current_user_can( $capability ) ) {
        return true;
    }
    if ( 'manage_options' !== $capability && current_user_can( 'manage_options' ) ) {
        return true;
    }
    if ( 'activate_plugins' !== $capability && current_user_can( 'activate_plugins' ) ) {
        return true;
    }
    return false;
}

if ( ! function_exists( 'bookaro_booking_button_anchor' ) ) {
    /**
     * Fragment used to scroll to booking buttons on listing pages.
     */
    function bookaro_booking_button_anchor(): string {
        return '#bookaro-booking-btn';
    }
}

if ( ! function_exists( 'bookaro_append_booking_button_anchor' ) ) {
    /**
     * Append the booking button anchor to a listing URL when missing.
     */
    function bookaro_append_booking_button_anchor( string $url ): string {
        $url = trim( $url );
        if ( $url === '' ) {
            return '';
        }
        if ( strpos( $url, '#' ) !== false ) {
            return $url;
        }
        return $url . bookaro_booking_button_anchor();
    }
}

/**
 * Resolve the upgrade URL for Lite upsell links.
 */
function bookaro_get_upgrade_url(): string {
    $default = defined( 'BOOKARO_DEFAULT_UPGRADE_URL' ) ? BOOKARO_DEFAULT_UPGRADE_URL : '';
    $url = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'bookaro_upgrade_url', $default )
        : $default;
    if ( '' === $url ) {
        $url = $default;
    }
    $url = (string) apply_filters( 'bookaro_upgrade_url', $url );
    return $url;
}

/**
 * Get currency settings for formatting (payments gateway-driven).
 *
 * @return array<string,mixed>
 */
function bookaro_get_currency_settings(): array {
    $defaults = [
        'symbol'             => '$',
        'decimals'           => 2,
        'decimal_separator'  => '.',
        'thousand_separator' => ',',
        'format'             => '%1$s%2$s',
    ];

    $settings = [];
    if ( function_exists( 'bookaro_get_payments_gateway' ) ) {
        $settings = bookaro_get_payments_gateway()->get_currency_settings();
    }
    if ( ! is_array( $settings ) ) {
        $settings = [];
    }

    $settings = array_merge( $defaults, $settings );
    if ( function_exists( 'bookaro_get_option' ) ) {
        $override_symbol = (string) bookaro_get_option( 'currency_symbol', '' );
        if ( $override_symbol !== '' ) {
            $settings['symbol'] = $override_symbol;
        }
    }
    $settings['format'] = str_replace( '&nbsp;', ' ', (string) ( $settings['format'] ?? '%1$s%2$s' ) );
    $settings['decimals'] = (int) ( $settings['decimals'] ?? 2 );

    return $settings;
}

/**
 * Format a price using the active payments gateway.
 */
function bookaro_format_price( $amount, bool $plain = true ): string {
    $amount = (float) $amount;
    $formatted = '';

    if ( function_exists( 'bookaro_get_payments_gateway' ) ) {
        $formatted = bookaro_get_payments_gateway()->format_price( $amount );
    }

    if ( ! is_string( $formatted ) || '' === $formatted ) {
        $settings = bookaro_get_currency_settings();
        $formatted_number = number_format(
            $amount,
            (int) $settings['decimals'],
            (string) $settings['decimal_separator'],
            (string) $settings['thousand_separator']
        );
        $formatted = sprintf(
            (string) $settings['format'],
            (string) $settings['symbol'],
            $formatted_number
        );
    }

    if ( $plain ) {
        $formatted = wp_strip_all_tags( $formatted );
        $formatted = str_replace( '&nbsp;', ' ', $formatted );
        $formatted = html_entity_decode( $formatted, ENT_QUOTES, 'UTF-8' );
    }

    return $formatted;
}

/**
 * Parse a localized price string into a float.
 */
function bookaro_parse_price_string( $raw ): float {
    if ( is_int( $raw ) || is_float( $raw ) ) {
        return (float) $raw;
    }
    if ( is_numeric( $raw ) ) {
        return (float) $raw;
    }
    if ( null === $raw ) {
        return 0.0;
    }
    $raw = trim( (string) $raw );
    if ( '' === $raw ) {
        return 0.0;
    }

    $filtered = preg_replace( "/[^0-9,\\.\\-+'\\s]/", '', $raw );
    if ( ! is_string( $filtered ) || '' === $filtered ) {
        return 0.0;
    }

    $filtered = str_replace( [ ' ', "'" ], '', $filtered );

    $last_comma = strrpos( $filtered, ',' );
    $last_dot   = strrpos( $filtered, '.' );

    if ( false !== $last_comma && false !== $last_dot ) {
        $decimal_sep  = $last_comma > $last_dot ? ',' : '.';
        $thousand_sep = $decimal_sep === ',' ? '.' : ',';
        $normalized = str_replace( $thousand_sep, '', $filtered );
        $normalized = str_replace( $decimal_sep, '.', $normalized );
        return (float) $normalized;
    }

    $sep = false !== $last_comma ? ',' : ( false !== $last_dot ? '.' : '' );
    if ( '' !== $sep ) {
        $settings = function_exists( 'bookaro_get_currency_settings' )
            ? bookaro_get_currency_settings()
            : [];
        $wc_dec  = (string) ( $settings['decimal_separator'] ?? '' );
        $wc_thou = (string) ( $settings['thousand_separator'] ?? '' );
        $decimals = isset( $settings['decimals'] ) ? (int) $settings['decimals'] : 2;

        $occurrences = substr_count( $filtered, $sep );
        $parts = explode( $sep, $filtered );
        $digits_after = strlen( end( $parts ) );

        $use_decimal = false;
        $use_thousand = false;

        if ( $occurrences > 1 ) {
            $use_thousand = true;
        } else {
            if ( '' !== $wc_dec && $sep === $wc_dec ) {
                $use_decimal = true;
            } elseif ( '' !== $wc_thou && $sep === $wc_thou ) {
                $use_thousand = true;
            }

            if ( $digits_after > 0 ) {
                if ( $decimals > 0 && $digits_after <= $decimals ) {
                    $use_decimal = true;
                    $use_thousand = false;
                } elseif ( 3 === $digits_after && strlen( $parts[0] ) > 0 ) {
                    if ( ! $use_decimal ) {
                        $use_thousand = true;
                    }
                } elseif ( $digits_after > 3 ) {
                    $use_decimal = true;
                    $use_thousand = false;
                }
            } else {
                $use_thousand = true;
            }
        }

        if ( $use_decimal ) {
            $normalized = str_replace( $sep, '.', $filtered );
        } else {
            $normalized = str_replace( $sep, '', $filtered );
        }
        return (float) $normalized;
    }

    $normalized = preg_replace( '/[^0-9\\-+]/', '', $filtered );
    if ( ! is_string( $normalized ) || '' === $normalized ) {
        return 0.0;
    }
    return (float) $normalized;
}

/**
 * Resolve the listing post type based on integration mode.
 */
function bookaro_get_listing_post_type(): string {
    $mode = bookaro_get_integration_mode();
    $types = function_exists( 'bookaro_get_allowed_listing_post_types' )
        ? bookaro_get_allowed_listing_post_types()
        : [];
    $post_type = $types[0] ?? 'job_listing';
    return (string) apply_filters( 'bookaro_listing_post_type', $post_type, $mode );
}

/**
 * Get allowed listing post types for bookings.
 *
 * @return array
 */
function bookaro_get_allowed_listing_post_types(): array {
    $mode = bookaro_get_integration_mode();
    $defaults = ( 'mylisting' === $mode ) ? [ 'job_listing' ] : [ BOOKARO_PROVIDER_POST_TYPE ];
    $settings = get_option( 'bookaro_settings', [] );
    $configured = [];
    if ( is_array( $settings ) ) {
        if ( ! empty( $settings['allowed_listing_post_types'] ) && is_array( $settings['allowed_listing_post_types'] ) ) {
            $configured = $settings['allowed_listing_post_types'];
        } elseif ( ! empty( $settings['listing_post_type'] ) ) {
            $configured = [ $settings['listing_post_type'] ];
        }
    }
    $configured = array_values( array_unique( array_filter( array_map( 'sanitize_key', $configured ) ) ) );
    if ( ! empty( $configured ) ) {
        $defaults = $configured;
    }
    $types = apply_filters( 'bookaro_allowed_listing_post_types', $defaults, $mode );
    if ( ! is_array( $types ) ) {
        $types = $defaults;
    }
    $types = array_values( array_unique( array_filter( array_map( 'sanitize_key', $types ) ) ) );
    return $types ?: $defaults;
}

/**
 * Get allowed listing pages for bookings.
 *
 * @return array
 */
function bookaro_get_allowed_listing_pages(): array {
    $settings = get_option( 'bookaro_settings', [] );
    $pages = [];
    if ( is_array( $settings ) && ! empty( $settings['allowed_listing_pages'] ) && is_array( $settings['allowed_listing_pages'] ) ) {
        $pages = $settings['allowed_listing_pages'];
    }
    $pages = array_values( array_unique( array_filter( array_map( 'absint', $pages ) ) ) );
    $pages = apply_filters( 'bookaro_allowed_listing_pages', $pages );
    if ( ! is_array( $pages ) ) {
        $pages = [];
    }
    $pages = array_values( array_unique( array_filter( array_map( 'absint', $pages ) ) ) );
    return $pages;
}

/**
 * Resolve a page override for a listing post type + context.
 *
 * @param string $mode     Integration mode (auto|mylisting).
 * @param string $post_type Listing post type slug.
 * @param string $context  booking|provider_dashboard|customer_dashboard
 */
function bookaro_get_page_for_context( string $mode, string $post_type, string $context ): int {
    $mode = sanitize_key( $mode );
    if ( '' === $mode && function_exists( 'bookaro_get_integration_mode' ) ) {
        $mode = bookaro_get_integration_mode();
    }
    $post_type = sanitize_key( $post_type );
    $context = sanitize_key( $context );
    if ( $post_type === '' || ! in_array( $context, [ 'booking', 'provider_dashboard', 'customer_dashboard' ], true ) ) {
        return 0;
    }

    $page_id = bookaro_resolve_page_override( $post_type, $context );
    $page_id = (int) apply_filters( 'bookaro_page_for_context', $page_id, $mode, $post_type, $context );
    return $page_id;
}

/**
 * Resolve a per-post-type page override for booking or dashboard pages.
 *
 * @param string $post_type Listing post type slug.
 * @param string $kind      booking|provider_dashboard|customer_dashboard
 */
function bookaro_resolve_page_override( string $post_type, string $kind ): int {
    $post_type = sanitize_key( $post_type );
    $kind = sanitize_key( $kind );
    if ( $post_type === '' || ! in_array( $kind, [ 'booking', 'provider_dashboard', 'customer_dashboard' ], true ) ) {
        return 0;
    }

    $settings = get_option( 'bookaro_settings', [] );
    $overrides = [];
    if ( is_array( $settings ) && ! empty( $settings['listing_post_type_pages'] ) && is_array( $settings['listing_post_type_pages'] ) ) {
        $overrides = $settings['listing_post_type_pages'];
    }
    $page_id = 0;
    if ( isset( $overrides[ $post_type ] ) && is_array( $overrides[ $post_type ] ) ) {
        $page_id = absint( $overrides[ $post_type ][ $kind ] ?? 0 );
    }

    $global_key_map = [
        'booking' => 'booking_page_id',
        'provider_dashboard' => 'provider_dashboard_page_id',
        'customer_dashboard' => 'customer_dashboard_page_id',
    ];
    $global_key = $global_key_map[ $kind ] ?? '';
    if ( $page_id < 1 && $global_key !== '' && is_array( $settings ) ) {
        $global_page_id = absint( $settings[ $global_key ] ?? 0 );
        if ( $global_page_id > 0 ) {
            $page_id = $global_page_id;
        }
    }
    if ( $page_id > 0 && 'page' !== get_post_type( $page_id ) ) {
        $page_id = 0;
    }

    return $page_id;
}

/**
 * Resolve the booking page URL for a listing post type.
 */
function bookaro_get_booking_page_url( array $args = [] ): string {
    $listing_id = absint( $args['listing_id'] ?? 0 );
    $post_type = '';
    if ( $listing_id > 0 ) {
        $post_type = get_post_type( $listing_id );
    }
    if ( $post_type === '' && ! empty( $args['post_type'] ) ) {
        $post_type = sanitize_key( (string) $args['post_type'] );
    }
    if ( $post_type === '' && function_exists( 'get_the_ID' ) ) {
        $current_id = (int) get_the_ID();
        if ( $current_id > 0 ) {
            $post_type = get_post_type( $current_id );
        }
    }
    if ( $post_type === '' ) {
        return '';
    }

    $mode = function_exists( 'bookaro_get_integration_mode' ) ? bookaro_get_integration_mode() : 'auto';
    $page_id = bookaro_get_page_for_context( $mode, $post_type, 'booking' );
    if ( $page_id < 1 ) {
        return '';
    }
    $url = get_permalink( $page_id );
    $url = (string) apply_filters( 'bookaro_booking_page_url', $url, $args );
    return $url;
}

/**
 * Resolve the provider dashboard page URL for the current context.
 */
function bookaro_get_provider_dashboard_url( array $args = [] ): string {
    $args['context'] = 'provider_dashboard';
    return bookaro_get_dashboard_url( $args );
}

/**
 * Resolve the customer dashboard page URL for the current context.
 */
function bookaro_get_customer_dashboard_url( array $args = [] ): string {
    $args['context'] = 'customer_dashboard';
    return bookaro_get_dashboard_url( $args );
}

/**
 * Check if we're viewing a single allowed listing post type.
 */
function bookaro_is_listing_singular(): bool {
    if ( ! function_exists( 'is_singular' ) ) {
        return false;
    }
    $types = bookaro_get_allowed_listing_post_types();
    if ( empty( $types ) || ! is_singular( $types ) ) {
        return false;
    }
    if ( is_singular( 'page' ) ) {
        $allowed_pages = bookaro_get_allowed_listing_pages();
        if ( ! empty( $allowed_pages ) ) {
            $page_id = function_exists( 'get_queried_object_id' ) ? (int) get_queried_object_id() : 0;
            if ( $page_id > 0 && ! in_array( $page_id, $allowed_pages, true ) ) {
                return false;
            }
        }
    }
    return true;
}

/**
 * Check whether a listing belongs to an allowed post type.
 */
function bookaro_is_allowed_listing( int $listing_id ): bool {
    if ( $listing_id < 1 ) {
        return false;
    }
    $type = get_post_type( $listing_id );
    if ( ! $type ) {
        return false;
    }
    if ( ! in_array( $type, bookaro_get_allowed_listing_post_types(), true ) ) {
        return false;
    }
    if ( 'page' === $type ) {
        $allowed_pages = bookaro_get_allowed_listing_pages();
        if ( ! empty( $allowed_pages ) && ! in_array( $listing_id, $allowed_pages, true ) ) {
            return false;
        }
    }
    return true;
}

/**
 * Get the selected Lite listing ID.
 */
function bookaro_get_lite_listing_id(): int {
    $listing_id = 0;
    return (int) apply_filters( 'bookaro_lite_listing_id', $listing_id );
}

/**
 * Check if the listing is allowed under Lite limits.
 */
function bookaro_is_lite_listing_allowed( int $listing_id ): bool {
    $listing_id = absint( $listing_id );
    if ( $listing_id < 1 ) {
        return false;
    }
    if ( function_exists( 'bookaro_is_pro_active' ) && bookaro_is_pro_active() ) {
        return true;
    }
    return (bool) apply_filters( 'bookaro_lite_listing_allowed', true, $listing_id, 0 );
}

/**
 * Resolve the owner user ID for a listing.
 */
function bookaro_get_listing_owner_id( int $listing_id ): int {
    $owner_id = 0;
    $post = null;
    if ( $listing_id > 0 ) {
        $post = get_post( $listing_id );
        if ( $post ) {
            $owner_id = (int) $post->post_author;
        }
    }

    $resolution_mode = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'provider_resolution_mode', '' )
        : '';
    $resolution_mode = sanitize_key( $resolution_mode );
    $meta_key = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'provider_meta_key', '' )
        : '';
    $meta_key = sanitize_key( $meta_key );
    if ( $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 !== '' ) {
            $meta_key = $legacy_meta_key;
        }
    }
    if ( ! in_array( $resolution_mode, [ 'author', 'meta_key' ], true ) ) {
        $resolution_mode = $meta_key !== '' ? 'meta_key' : 'author';
    }

    if ( 'meta_key' === $resolution_mode && $meta_key !== '' && $listing_id > 0 ) {
        $meta_value = get_post_meta( $listing_id, $meta_key, true );
        $meta_id = absint( $meta_value );
        if ( $meta_id > 0 && get_user_by( 'ID', $meta_id ) ) {
            $owner_id = $meta_id;
        }
    }

    $owner_id = (int) apply_filters( 'bookaro_listing_owner_id', $owner_id, $listing_id );
    return $owner_id;
}

/**
 * Resolve a listing ID for a provider using configured resolution rules.
 */
function bookaro_get_listing_id_for_provider( int $provider_id ): int {
    $provider_id = absint( $provider_id );
    if ( $provider_id < 1 || ! function_exists( 'bookaro_get_allowed_listing_post_types' ) ) {
        return 0;
    }

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

    $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';
    }

    $args = [
        'post_type'      => $types,
        'post_status'    => 'publish',
        'posts_per_page' => 5,
        'orderby'        => 'menu_order title',
        'order'          => 'ASC',
        'fields'         => 'ids',
    ];
    if ( 'meta_key' === $resolution_mode && $provider_meta_key !== '' ) {
        $args['meta_query'] = [
            [
                'key'     => $provider_meta_key,
                'value'   => $provider_id,
                'compare' => '=',
                'type'    => 'NUMERIC',
            ],
        ];
    } else {
        $args['author'] = $provider_id;
    }

    $args = apply_filters( 'bookaro_provider_listings_query_args', $args, $provider_id );
    $ids = get_posts( $args );
    foreach ( $ids as $listing_id ) {
        $listing_id = (int) $listing_id;
        if ( $listing_id > 0 && function_exists( 'bookaro_is_allowed_listing' ) && bookaro_is_allowed_listing( $listing_id ) ) {
            return $listing_id;
        }
    }

    return 0;
}

/**
 * Resolve a sample listing ID for Generic admin validation.
 */
function bookaro_get_validation_sample_listing_id( int $preferred_listing_id = 0 ): int {
    $preferred_listing_id = absint( $preferred_listing_id );
    if ( $preferred_listing_id > 0 && function_exists( 'bookaro_is_allowed_listing' ) && bookaro_is_allowed_listing( $preferred_listing_id ) ) {
        return $preferred_listing_id;
    }

    $configured_listing_id = function_exists( 'bookaro_get_option' )
        ? absint( bookaro_get_option( 'generic_setup_listing_id', 0 ) )
        : 0;
    if ( $configured_listing_id > 0 && function_exists( 'bookaro_is_allowed_listing' ) && bookaro_is_allowed_listing( $configured_listing_id ) ) {
        return $configured_listing_id;
    }

    $types = function_exists( 'bookaro_get_allowed_listing_post_types' )
        ? bookaro_get_allowed_listing_post_types()
        : [];
    if ( empty( $types ) ) {
        return 0;
    }

    $ids = get_posts( [
        'post_type'      => $types,
        'post_status'    => [ 'publish', 'private', 'draft', 'pending' ],
        'posts_per_page' => 20,
        'orderby'        => 'date',
        'order'          => 'DESC',
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ] );
    if ( ! is_array( $ids ) ) {
        $ids = [];
    }

    foreach ( $ids as $listing_id ) {
        $listing_id = absint( $listing_id );
        if ( $listing_id < 1 ) {
            continue;
        }
        if ( function_exists( 'bookaro_is_allowed_listing' ) && ! bookaro_is_allowed_listing( $listing_id ) ) {
            continue;
        }
        return $listing_id;
    }

    return 0;
}

/**
 * Resolve a sample provider ID for Generic admin validation.
 */
function bookaro_get_validation_sample_provider_id( int $listing_id = 0, int $preferred_provider_id = 0 ): int {
    $preferred_provider_id = absint( $preferred_provider_id );
    if ( $preferred_provider_id > 0 && get_user_by( 'ID', $preferred_provider_id ) ) {
        return $preferred_provider_id;
    }

    $listing_id = absint( $listing_id );
    if ( $listing_id > 0 && function_exists( 'bookaro_get_listing_owner_id' ) ) {
        $owner_id = (int) bookaro_get_listing_owner_id( $listing_id );
        if ( $owner_id > 0 && get_user_by( 'ID', $owner_id ) ) {
            return $owner_id;
        }
    }

    $default_provider_id = function_exists( 'bookaro_get_option' )
        ? absint( bookaro_get_option( 'default_provider_id', 0 ) )
        : 0;
    if ( $default_provider_id > 0 && get_user_by( 'ID', $default_provider_id ) ) {
        return $default_provider_id;
    }

    if ( $listing_id > 0 ) {
        $post = get_post( $listing_id );
        if ( $post instanceof \WP_Post ) {
            $author_id = absint( $post->post_author );
            if ( $author_id > 0 && get_user_by( 'ID', $author_id ) ) {
                return $author_id;
            }
        }
    }

    return 0;
}

/**
 * Run Generic mode validation checks with pass/fail outcomes.
 *
 * @return array{
 *   mode:string,
 *   sample_listing_id:int,
 *   sample_provider_id:int,
 *   checks:array<int,array<string,mixed>>,
 *   failed_count:int
 * }
 */
function bookaro_run_generic_admin_validation( array $args = [] ): array {
    $mode = function_exists( 'bookaro_get_integration_mode' )
        ? (string) bookaro_get_integration_mode()
        : 'auto';
    $settings_url = admin_url( 'admin.php?page=bookaro-booking-settings&tab=general' );
    $wizard_pages_url = admin_url( 'admin.php?page=bookaro-setup-wizard&step=pages' );
    $wizard_services_url = admin_url( 'admin.php?page=bookaro-setup-wizard&step=services' );
    $wizard_listings_url = admin_url( 'admin.php?page=bookaro-setup-wizard&step=listings' );
    $service_type = defined( 'BOOKARO_SERVICE_POST_TYPE' ) ? BOOKARO_SERVICE_POST_TYPE : 'bookaro_service';
    $services_url = admin_url( 'edit.php?post_type=' . $service_type );
    $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';
    }
    $single_business = ( 'single_business' === $site_type );

    $sample_listing_id = bookaro_get_validation_sample_listing_id( absint( $args['listing_id'] ?? 0 ) );
    $sample_provider_id = bookaro_get_validation_sample_provider_id(
        $sample_listing_id,
        absint( $args['provider_id'] ?? 0 )
    );

    $checks = [];
    if ( 'generic' !== $mode ) {
        $checks[] = [
            'id' => 'generic_validation_mode',
            'label' => __( 'Generic validation availability', 'mybookaro' ),
            'state' => 'info',
            'value' => __( 'Not required', 'mybookaro' ),
            'detail' => __( 'These checks apply only when the site is running in the Generic Directory lane.', 'mybookaro' ),
            'fix' => __( 'If this site should use Generic Directory, switch Directory lane routing to Generic Directory and run these checks again.', 'mybookaro' ),
            'action_url' => $settings_url,
            'action_label' => __( 'Open settings', 'mybookaro' ),
        ];

        return [
            'mode' => $mode,
            'sample_listing_id' => $sample_listing_id,
            'sample_provider_id' => $sample_provider_id,
            'checks' => $checks,
            'failed_count' => 0,
        ];
    }

    // Check 1: provider resolution for a sample listing object.
    $provider_state = 'error';
    $provider_value = __( 'Fail', 'mybookaro' );
    $provider_detail = __( 'Bookaro could not find a valid sample listing for provider checks. Until there is one, it cannot confirm who should receive bookings.', 'mybookaro' );
    $provider_fix = __( 'Create or select a valid sample listing for your allowed listing type, then run Generic validation again.', 'mybookaro' );
    $provider_next_step = __( 'Next step: Create or select the sample listing, save any mapping changes, then run Generic validation again.', 'mybookaro' );

    if ( $single_business ) {
        if ( $sample_provider_id > 0 && get_user_by( 'ID', $sample_provider_id ) ) {
            $provider_state = 'ok';
            $provider_value = __( 'Pass', 'mybookaro' );
            $provider_detail = sprintf(
                /* translators: %d: provider user id */
                __( 'Default Provider user #%d is available for Single Business booking. Provider-first booking pages have a provider context to work with.', 'mybookaro' ),
                $sample_provider_id
            );
            $provider_fix = '';
            $provider_next_step = '';
        } else {
            $provider_state = 'error';
            $provider_value = __( 'Fail', 'mybookaro' );
            $provider_detail = __( 'Default Provider is missing or invalid. Until it is set, Single Business booking pages cannot resolve which provider should receive bookings.', 'mybookaro' );
            $provider_fix = __( 'Set Default Provider in Settings > General, then rerun validation.', 'mybookaro' );
            $provider_next_step = __( 'Next step: Open Settings > General or Setup Wizard > Listings, set Default Provider, save, then run validation again.', 'mybookaro' );
        }
    } elseif ( $sample_listing_id > 0 ) {
        $post = get_post( $sample_listing_id );
        if ( $post instanceof \WP_Post ) {
            $author_owner = absint( $post->post_author );
            $resolution_mode = function_exists( 'bookaro_get_option' )
                ? sanitize_key( (string) bookaro_get_option( 'provider_resolution_mode', '' ) )
                : '';
            $provider_meta_key = function_exists( 'bookaro_get_option' )
                ? sanitize_key( (string) bookaro_get_option( 'provider_meta_key', '' ) )
                : '';
            if ( '' === $provider_meta_key ) {
                $provider_meta_key = function_exists( 'bookaro_get_option' )
                    ? sanitize_key( (string) bookaro_get_option( 'listing_provider_meta_key', '' ) )
                    : '';
            }
            if ( ! in_array( $resolution_mode, [ 'author', 'meta_key' ], true ) ) {
                $resolution_mode = ( '' !== $provider_meta_key ) ? 'meta_key' : 'author';
            }

            $expected_owner = $author_owner;
            $expected_source = __( 'Listing Author', 'mybookaro' );
            $meta_value = 0;
            $meta_owner_exists = false;
            if ( 'meta_key' === $resolution_mode ) {
                if ( '' !== $provider_meta_key ) {
                    $meta_value = absint( get_post_meta( $sample_listing_id, $provider_meta_key, true ) );
                    $meta_owner_exists = ( $meta_value > 0 ) && (bool) get_user_by( 'ID', $meta_value );
                    if ( $meta_owner_exists ) {
                        $expected_owner = $meta_value;
                        $expected_source = __( 'Listing Meta Key', 'mybookaro' );
                    } else {
                        $expected_source = __( 'Listing Meta Key', 'mybookaro' );
                    }
                } else {
                    $expected_source = __( 'Listing Meta Key', 'mybookaro' );
                }
            }

            $resolved_owner = function_exists( 'bookaro_get_listing_owner_id' )
                ? absint( bookaro_get_listing_owner_id( $sample_listing_id ) )
                : 0;
            $resolved_owner_exists = $resolved_owner > 0 && (bool) get_user_by( 'ID', $resolved_owner );

            if ( 'meta_key' === $resolution_mode && '' === $provider_meta_key ) {
                $provider_state = 'error';
                $provider_value = __( 'Fail', 'mybookaro' );
                $provider_detail = __( 'Provider Mapping is set to Listing Meta Key, but Provider Meta Key is empty. Bookaro cannot read the provider user ID from your listings until you fill that field.', 'mybookaro' );
                $provider_fix = __( 'Set Provider Meta Key to the listing field that stores the provider user ID, or switch Provider Mapping back to Listing Author.', 'mybookaro' );
                $provider_next_step = __( 'Next step: Open Setup Wizard > Listings, fill Provider Meta Key (for example provider_user_id) or switch back to Listing Author, save, then run validation again.', 'mybookaro' );
            } elseif ( 'meta_key' === $resolution_mode && ! $meta_owner_exists ) {
                $provider_state = 'warn';
                $provider_value = __( 'Warning', 'mybookaro' );
                $provider_detail = sprintf(
                    /* translators: 1: listing id, 2: provider meta key */
                    __( 'Sample listing #%1$d is using Listing Meta Key provider mapping, but the `%2$s` field does not contain a valid provider user ID. Bookings may fall back to the wrong provider until that field is fixed.', 'mybookaro' ),
                    $sample_listing_id,
                    $provider_meta_key
                );
                $provider_fix = __( 'Populate the configured Provider Meta Key with a valid provider user ID for the sample listing, or switch Provider Mapping back to Listing Author.', 'mybookaro' );
                $provider_next_step = __( 'Next step: Update the sample listing provider field or switch the mapping mode, save, then run validation again.', 'mybookaro' );
            } elseif ( ! $resolved_owner_exists ) {
                $provider_state = 'error';
                $provider_value = __( 'Fail', 'mybookaro' );
                $provider_detail = sprintf(
                    /* translators: 1: listing id, 2: resolved provider id */
                    __( 'Sample listing #%1$d currently resolves to provider user #%2$d, but that user does not exist. Customers would not reach a valid provider until this is fixed.', 'mybookaro' ),
                    $sample_listing_id,
                    $resolved_owner
                );
                $provider_fix = __( 'Fix listing ownership or provider metadata so the sample listing points to an existing provider user.', 'mybookaro' );
                $provider_next_step = __( 'Next step: Correct the listing owner or provider meta value, save, then run validation again.', 'mybookaro' );
            } elseif ( $resolved_owner !== $expected_owner ) {
                $provider_state = 'error';
                $provider_value = __( 'Fail', 'mybookaro' );
                $provider_detail = sprintf(
                    /* translators: 1: listing id, 2: expected provider id, 3: resolved provider id, 4: expected source */
                    __( 'Sample listing #%1$d should resolve to provider #%2$d from %4$s, but Bookaro currently resolves provider #%3$d. Bookings can route to the wrong provider until those two match.', 'mybookaro' ),
                    $sample_listing_id,
                    $expected_owner,
                    $resolved_owner,
                    $expected_source
                );
                $provider_fix = __( 'Review Provider Mapping and the sample listing ownership/provider metadata so Bookaro resolves the expected provider.', 'mybookaro' );
                $provider_next_step = __( 'Next step: Open Setup Wizard > Listings, confirm Provider Mapping, update the sample listing if needed, save, then run validation again.', 'mybookaro' );
            } else {
                $provider_state = 'ok';
                $provider_value = __( 'Pass', 'mybookaro' );
                $provider_detail = sprintf(
                    /* translators: 1: listing id, 2: resolved provider id, 3: resolution source label */
                    __( 'Sample listing #%1$d resolves to provider #%2$d using %3$s. Bookings should reach the expected provider for this setup.', 'mybookaro' ),
                    $sample_listing_id,
                    $resolved_owner,
                    $expected_source
                );
                $provider_fix = '';
                $provider_next_step = '';
            }
        }
    }

    $checks[] = [
        'id' => 'provider_resolution_sample',
        'label' => __( 'Provider mapping for a sample listing', 'mybookaro' ),
        'state' => $provider_state,
        'value' => $provider_value,
        'detail' => $provider_detail,
        'fix' => $provider_fix,
        'next_step' => $provider_next_step,
        'action_url' => $wizard_listings_url,
        'action_label' => __( 'Open listings step', 'mybookaro' ),
    ];

    // Check 2: service resolution for sample provider/listing.
    $service_state = 'error';
    $service_value = __( 'Fail', 'mybookaro' );
    $service_detail = __( 'Service checks are unavailable right now. Until they run, Bookaro cannot confirm which services customers should see.', 'mybookaro' );
    $service_fix = __( 'Review the Services step and the sample services, then run validation again.', 'mybookaro' );
    $service_next_step = __( 'Next step: Open Setup Wizard > Services, confirm Services Source, then run validation again.', 'mybookaro' );
    if ( $single_business ) {
        if ( $sample_provider_id < 1 || ! get_user_by( 'ID', $sample_provider_id ) ) {
            $service_state = 'error';
            $service_value = __( 'Fail', 'mybookaro' );
            $service_detail = __( 'Default Provider is missing, so Bookaro cannot check which services should appear on Single Business booking pages.', 'mybookaro' );
            $service_fix = __( 'Set Default Provider in Settings > General (or Setup Wizard > Listings), then rerun validation.', 'mybookaro' );
            $service_next_step = __( 'Next step: Set Default Provider, save, then run validation again.', 'mybookaro' );
        } else {
            $available_services = function_exists( 'bookaro_get_generic_provider_context_services' )
                ? bookaro_get_generic_provider_context_services( $sample_provider_id )
                : [];
            $available_services = is_array( $available_services ) ? $available_services : [];
            $available_service_count = count( $available_services );

            if ( $available_service_count > 0 ) {
                $service_state = 'ok';
                $service_value = __( 'Pass', 'mybookaro' );
                $service_detail = sprintf(
                    /* translators: 1: provider user id, 2: available services count */
                    __( 'Default Provider #%1$d currently resolves %2$d services. Single Business booking pages should have service choices to show.', 'mybookaro' ),
                    $sample_provider_id,
                    $available_service_count
                );
                $service_fix = '';
                $service_next_step = '';
            } else {
                $service_state = 'error';
                $service_value = __( 'Fail', 'mybookaro' );
                $service_detail = sprintf(
                    /* translators: %d: provider user id */
                    __( 'Default Provider #%d resolves zero services, so the Single Business booking UI cannot show any service choices yet.', 'mybookaro' ),
                    $sample_provider_id
                );
                $service_fix = __( 'Open Setup Wizard > Services to confirm Services Source, then create or assign at least one service for the Default Provider.', 'mybookaro' );
                $service_next_step = __( 'Next step: Confirm Services Source, add a service for the Default Provider, then run validation again.', 'mybookaro' );
            }
        }
    } elseif ( class_exists( '\\Bookaro\\Controller\\Rest\\ProviderServicesController' ) && method_exists( '\\Bookaro\\Controller\\Rest\\ProviderServicesController', 'get_admin_service_resolution_diagnostics' ) ) {
        $service_diag = \Bookaro\Controller\Rest\ProviderServicesController::get_admin_service_resolution_diagnostics( $sample_listing_id, $sample_provider_id );
        $service_state = sanitize_key( (string) ( $service_diag['state'] ?? 'info' ) );
        if ( ! in_array( $service_state, [ 'ok', 'warn', 'error', 'info' ], true ) ) {
            $service_state = 'info';
        }
        $service_value = (string) ( $service_diag['value'] ?? '' );
        if ( '' === $service_value ) {
            $service_value = ( 'ok' === $service_state ) ? __( 'Pass', 'mybookaro' ) : __( 'Fail', 'mybookaro' );
        }
        $service_detail = (string) ( $service_diag['detail'] ?? $service_detail );
        $service_fix = (string) ( $service_diag['fix'] ?? $service_fix );
        $service_next_step = (string) ( $service_diag['next_step'] ?? $service_next_step );
    }

    $checks[] = [
        'id' => 'service_resolution_sample',
        'label' => __( 'Services available for the sample booking path', 'mybookaro' ),
        'state' => $service_state,
        'value' => $service_value,
        'detail' => $service_detail,
        'fix' => $service_fix,
        'next_step' => $service_next_step,
        'action_url' => $services_url,
        'action_label' => __( 'Open services', 'mybookaro' ),
    ];

    // Check 3: booking button visibility rules.
    $button_state = 'error';
    $button_value = __( 'Fail', 'mybookaro' );
    $button_detail = __( 'Booking entry checks are unavailable right now. Until they run, Bookaro cannot confirm where customers should start a booking.', 'mybookaro' );
    $button_fix = __( 'Review booking placement settings and the Booking page assignment, then run validation again.', 'mybookaro' );
    $button_next_step = __( 'Next step: Open Setup Wizard > Services or Pages, confirm the booking entry path, then run validation again.', 'mybookaro' );
    if ( class_exists( '\\Bookaro\\Controller\\Frontend\\ShortcodesController' ) && method_exists( '\\Bookaro\\Controller\\Frontend\\ShortcodesController', 'get_admin_booking_button_visibility_diagnostics' ) ) {
        $button_diag = \Bookaro\Controller\Frontend\ShortcodesController::get_admin_booking_button_visibility_diagnostics( $sample_listing_id );
        $button_state = sanitize_key( (string) ( $button_diag['state'] ?? 'info' ) );
        if ( ! in_array( $button_state, [ 'ok', 'warn', 'error', 'info' ], true ) ) {
            $button_state = 'info';
        }
        $button_value = (string) ( $button_diag['value'] ?? '' );
        if ( '' === $button_value ) {
            $button_value = ( 'ok' === $button_state ) ? __( 'Pass', 'mybookaro' ) : __( 'Fail', 'mybookaro' );
        }
        $button_detail = (string) ( $button_diag['detail'] ?? $button_detail );
        $button_fix = (string) ( $button_diag['fix'] ?? $button_fix );
        $button_next_step = (string) ( $button_diag['next_step'] ?? $button_next_step );
    }

    $checks[] = [
        'id' => 'booking_button_visibility_rules',
        'label' => __( 'Booking entry placement', 'mybookaro' ),
        'state' => $button_state,
        'value' => $button_value,
        'detail' => $button_detail,
        'fix' => $button_fix,
        'next_step' => $button_next_step,
        'action_url' => $wizard_services_url,
        'action_label' => __( 'Open services step', 'mybookaro' ),
    ];

    // Check 4: required page assignment.
    $pages_state = 'error';
    $pages_value = __( 'Fail', 'mybookaro' );
    $pages_detail = __( 'Bookaro could not find the context it needs for page checks. Until that is resolved, it cannot confirm which booking and dashboard pages are required.', 'mybookaro' );
    $pages_fix = __( 'Create or select a sample listing context, then assign the required pages in Setup Wizard > Pages.', 'mybookaro' );
    $pages_next_step = __( 'Next step: Open Setup Wizard > Pages, confirm the required pages for this setup, save, then run validation again.', 'mybookaro' );
    $sample_post_type = '';
    if ( $sample_listing_id > 0 ) {
        $sample_post_type = sanitize_key( (string) get_post_type( $sample_listing_id ) );
    }
    if ( $single_business && defined( 'BOOKARO_PROVIDER_POST_TYPE' ) ) {
        $sample_post_type = sanitize_key( (string) BOOKARO_PROVIDER_POST_TYPE );
    }

    if ( '' !== $sample_post_type && function_exists( 'bookaro_get_page_for_context' ) ) {
        $ui_mode = function_exists( 'bookaro_get_ui_mode' ) ? bookaro_get_ui_mode() : 'shortcodes';
        $booking_page_id = absint( bookaro_get_page_for_context( 'generic', $sample_post_type, 'booking' ) );
        $provider_page_id = absint( bookaro_get_page_for_context( 'generic', $sample_post_type, 'provider_dashboard' ) );
        $customer_page_id = absint( bookaro_get_page_for_context( 'generic', $sample_post_type, 'customer_dashboard' ) );

        if ( $single_business ) {
            $booking_page_id = max(
                $booking_page_id,
                function_exists( 'bookaro_get_option' ) ? absint( bookaro_get_option( 'booking_page_id', 0 ) ) : 0
            );
            $provider_page_id = max(
                $provider_page_id,
                function_exists( 'bookaro_get_option' ) ? absint( bookaro_get_option( 'provider_dashboard_page_id', 0 ) ) : 0
            );
            $customer_page_id = max(
                $customer_page_id,
                function_exists( 'bookaro_get_option' ) ? absint( bookaro_get_option( 'customer_dashboard_page_id', 0 ) ) : 0
            );
        }

        $missing = [];
        if ( $booking_page_id < 1 ) {
            $missing[] = __( 'Booking page', 'mybookaro' );
        }
        if ( 'shortcodes' === $ui_mode ) {
            if ( $provider_page_id < 1 ) {
                $missing[] = __( 'Provider dashboard page', 'mybookaro' );
            }
            if ( $customer_page_id < 1 ) {
                $missing[] = __( 'Customer dashboard page', 'mybookaro' );
            }
        }

        if ( empty( $missing ) ) {
            $pages_state = 'ok';
            $pages_value = __( 'Pass', 'mybookaro' );
            $pages_detail = 'shortcodes' === $ui_mode
                ? sprintf(
                    /* translators: %s: context label */
                    __( 'Required pages are assigned for %s. Customers and dashboard users have the pages Bookaro expects in Shortcodes mode.', 'mybookaro' ),
                    $single_business ? __( 'Single Business', 'mybookaro' ) : $sample_post_type
                )
                : sprintf(
                    /* translators: %s: context label */
                    __( 'The Booking page is assigned for %s. Dashboard pages are not required in this UI mode.', 'mybookaro' ),
                    $single_business ? __( 'Single Business', 'mybookaro' ) : $sample_post_type
                );
            $pages_fix = '';
            $pages_next_step = '';
        } else {
            $pages_state = 'error';
            $pages_value = __( 'Fail', 'mybookaro' );
            $pages_detail = sprintf(
                /* translators: 1: context label, 2: missing pages list */
                __( '%1$s is missing %2$s. Until those pages are assigned, customers or dashboard users can be sent to incomplete booking screens.', 'mybookaro' ),
                $single_business ? __( 'Single Business', 'mybookaro' ) : $sample_post_type,
                implode( ', ', $missing )
            );
            $pages_fix = $single_business
                ? __( 'Single Business requires a Booking page for provider-first entry. In Shortcodes mode it also requires Provider + Customer dashboard pages with the matching MyBookaro dashboard block or shortcode.', 'mybookaro' )
                : __( 'Assign all required pages in Setup Wizard > Pages and ensure dashboard pages contain the correct block/shortcode.', 'mybookaro' );
            $pages_next_step = __( 'Next step: Open Setup Wizard > Pages, assign every missing page, save, then run validation again.', 'mybookaro' );
        }
    }

    $checks[] = [
        'id' => 'required_page_assignment',
        'label' => __( 'Required pages for booking and dashboards', 'mybookaro' ),
        'state' => $pages_state,
        'value' => $pages_value,
        'detail' => $pages_detail,
        'fix' => $pages_fix,
        'next_step' => $pages_next_step,
        'action_url' => $wizard_pages_url,
        'action_label' => __( 'Open pages step', 'mybookaro' ),
    ];

    $failed_count = count( array_filter(
        $checks,
        static function ( array $check ): bool {
            $state = sanitize_key( (string) ( $check['state'] ?? '' ) );
            return in_array( $state, [ 'warn', 'error' ], true );
        }
    ) );

    return [
        'mode' => $mode,
        'sample_listing_id' => $sample_listing_id,
        'sample_provider_id' => $sample_provider_id,
        'checks' => $checks,
        'failed_count' => $failed_count,
    ];
}

/**
 * Deprecated: provider resolution is handled in bookaro_get_listing_owner_id().
 */
function bookaro_generic_provider_meta_owner_override( int $owner_id, int $listing_id ): int {
    return $owner_id;
}

/**
 * Check whether providers can manage services on the frontend.
 */
function bookaro_frontend_services_enabled(): bool {
    $enabled = false;
    $mode = function_exists( 'bookaro_get_integration_mode' )
        ? bookaro_get_integration_mode()
        : 'auto';
    if ( 'generic' === $mode && function_exists( 'bookaro_get_option' ) ) {
        $enabled = (bool) bookaro_get_option( 'provider_services_enabled', 1 );
    }

    return (bool) apply_filters( 'bookaro_frontend_services_enabled', $enabled, $mode );
}

/**
 * Get the maximum number of services allowed per site.
 * Use 0 for unlimited.
 */
function bookaro_get_service_limit(): int {
    $limit = (int) apply_filters( 'bookaro_service_limit', 0 );
    return max( 0, $limit );
}

/**
 * Count existing services excluding trash/auto-draft.
 */
function bookaro_count_services(): int {
    if ( function_exists( 'wp_count_posts' ) ) {
        $counts = wp_count_posts( BOOKARO_SERVICE_POST_TYPE );
        if ( $counts ) {
            $total = 0;
            foreach ( $counts as $status => $count ) {
                if ( in_array( $status, [ 'trash', 'auto-draft' ], true ) ) {
                    continue;
                }
                $total += (int) $count;
            }
            return $total;
        }
    }

    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
    $query = new \WP_Query( [
        'post_type'      => BOOKARO_SERVICE_POST_TYPE,
        'post_status'    => [ 'publish', 'draft', 'pending', 'private', 'future' ],
        'posts_per_page' => 1,
        'fields'         => 'ids',
    ] );
    return (int) $query->found_posts;
}

/**
 * Determine if the service limit has been reached.
 */
function bookaro_service_limit_reached(): bool {
    $limit = bookaro_get_service_limit();
    if ( 0 === $limit ) {
        return false;
    }
    return bookaro_count_services() >= $limit;
}

/**
 * Block service creation when the limit is reached.
 *
 * @param array $data    Sanitized post data.
 * @param array $postarr Raw post data.
 * @return array
 */
function bookaro_block_service_insert( array $data, array $postarr ): array {
    $post_type = $data['post_type'] ?? '';
    if ( BOOKARO_SERVICE_POST_TYPE !== $post_type ) {
        return $data;
    }
    if ( ! empty( $postarr['ID'] ) ) {
        return $data;
    }
    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
        return $data;
    }
    if ( ! bookaro_service_limit_reached() ) {
        return $data;
    }

    wp_die(
        esc_html__( 'Service limit reached for this site.', 'mybookaro' ),
        esc_html__( 'Service limit reached', 'mybookaro' ),
        [ 'response' => 403 ]
    );
}
add_filter( 'wp_insert_post_data', 'bookaro_block_service_insert', 10, 2 );

/**
 * Block service creation via REST when the limit is reached.
 *
 * @param \WP_Post|\stdClass $prepared_post Prepared post object.
 * @param \WP_REST_Request   $request       REST request.
 * @return \WP_Post|\stdClass|\WP_Error
 */
function bookaro_block_service_rest_insert( $prepared_post, \WP_REST_Request $request ) {
    if ( ! bookaro_service_limit_reached() ) {
        return $prepared_post;
    }
    if ( 'POST' !== $request->get_method() ) {
        return $prepared_post;
    }
    if ( $request->get_param( 'id' ) ) {
        return $prepared_post;
    }

    return new \WP_Error(
        'bookaro_service_limit',
        __( 'Service limit reached for this site.', 'mybookaro' ),
        [ 'status' => 403 ]
    );
}
add_filter( 'rest_pre_insert_' . BOOKARO_SERVICE_POST_TYPE, 'bookaro_block_service_rest_insert', 10, 2 );

/**
 * Get a cache stamp for listing services.
 */
function bookaro_get_services_cache_stamp(): int {
    return (int) round( microtime( true ) * 1000 );
}

/**
 * Get the global services cache version.
 */
function bookaro_get_services_cache_version(): int {
    if ( ! function_exists( 'get_option' ) ) {
        return 0;
    }
    return absint( get_option( 'bookaro_services_cache_version', 0 ) );
}

/**
 * Bump the global services cache version.
 */
function bookaro_bump_services_cache_version(): int {
    $stamp = bookaro_get_services_cache_stamp();
    if ( ! function_exists( 'get_option' ) || ! function_exists( 'update_option' ) ) {
        return $stamp;
    }
    if ( false === get_option( 'bookaro_services_cache_version', false ) ) {
        add_option( 'bookaro_services_cache_version', $stamp, '', false );
    } else {
        update_option( 'bookaro_services_cache_version', $stamp, false );
    }
    return $stamp;
}

/**
 * Get the listing services cache buster value.
 */
function bookaro_get_listing_services_cache_buster( int $listing_id ): int {
    if ( $listing_id < 1 ) {
        return 0;
    }
    return absint( get_post_meta( $listing_id, BOOKARO_SERVICES_CACHE_META, true ) );
}

/**
 * Bump the listing services cache buster value.
 */
function bookaro_bump_listing_services_cache_buster( int $listing_id ): int {
    if ( $listing_id < 1 ) {
        return 0;
    }
    $stamp = bookaro_get_services_cache_stamp();
    update_post_meta( $listing_id, BOOKARO_SERVICES_CACHE_META, $stamp );
    return $stamp;
}

/**
 * Build a cache key for listing services.
 */
function bookaro_get_listing_services_cache_key( int $listing_id, string $mode, string $context = '' ): string {
    if ( $listing_id < 1 ) {
        return '';
    }
    $mode = $mode !== '' ? $mode : bookaro_get_integration_mode();
    $modified = (int) get_post_modified_time( 'U', true, $listing_id );
    $buster = bookaro_get_listing_services_cache_buster( $listing_id );
    $global = bookaro_get_services_cache_version();
    $raw = implode( '|', [ $listing_id, $mode, $context, $modified, $buster, $global ] );
    return 'bookaro_services_' . md5( $raw );
}

/**
 * Determine whether listing services should be cached.
 */
function bookaro_should_cache_listing_services( int $listing_id, string $mode, string $context = '' ): bool {
    $should_cache = true;
    if ( function_exists( 'apply_filters' ) ) {
        $should_cache = (bool) apply_filters( 'bookaro_cache_listing_services', $should_cache, $listing_id, $mode, $context );
    }
    return $should_cache;
}

/**
 * Get listing services cache TTL.
 */
function bookaro_get_listing_services_cache_ttl( int $listing_id, string $mode, string $context = '' ): int {
    $ttl = 900;
    if ( function_exists( 'apply_filters' ) ) {
        $ttl = (int) apply_filters( 'bookaro_services_cache_ttl', $ttl, $listing_id, $mode, $context );
    }
    return max( 0, $ttl );
}

/**
 * Resolve listing services through integrations.
 *
 * @return array
 */
function bookaro_resolve_listing_services( int $listing_id, string $mode = '' ): array {
    if ( $listing_id < 1 ) {
        return [];
    }
    $mode = $mode ?: bookaro_get_integration_mode();
    $context = 'integration';
    $cache_key = bookaro_get_listing_services_cache_key( $listing_id, $mode, $context );
    $use_cache = $cache_key !== '' && bookaro_should_cache_listing_services( $listing_id, $mode, $context );

    if ( $use_cache ) {
        $cached = get_transient( $cache_key );
        if ( is_array( $cached ) ) {
            return $cached;
        }
    }

    $services = apply_filters( 'bookaro_listing_services', [], $listing_id, $mode );
    $services = is_array( $services ) ? $services : [];

    if ( $use_cache ) {
        $ttl = bookaro_get_listing_services_cache_ttl( $listing_id, $mode, $context );
        if ( $ttl > 0 ) {
            set_transient( $cache_key, $services, $ttl );
        }
    }

    return $services;
}

/**
 * Get services model for generic mode.
 *
 * @return string admin_only|provider_only|hybrid
 */
function bookaro_get_services_model(): string {
    $model = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'services_model', '' )
        : '';
    $model = sanitize_key( $model );
    if ( 'admin_catalog' === $model ) {
        $model = 'admin_only';
    }
    if ( in_array( $model, [ 'admin_only', 'provider_only', 'hybrid' ], true ) ) {
        return $model;
    }

    $legacy = function_exists( 'bookaro_get_option' )
        ? (string) bookaro_get_option( 'generic_services_mode', '' )
        : '';
    $legacy = sanitize_key( $legacy );
    if ( 'admin_catalog' === $legacy ) {
        return 'admin_only';
    }
    if ( in_array( $legacy, [ 'provider_only', 'hybrid' ], true ) ) {
        return $legacy;
    }

    return 'hybrid';
}

/**
 * Can providers create custom services (hybrid only).
 */
function bookaro_allow_provider_custom_services(): bool {
    $model = bookaro_get_services_model();
    if ( 'provider_only' === $model ) {
        return true;
    }
    if ( 'hybrid' !== $model ) {
        return false;
    }
    $enabled = function_exists( 'bookaro_get_option' )
        ? (int) bookaro_get_option( 'allow_provider_custom_services', 1 )
        : 1;
    return (bool) apply_filters( 'bookaro_allow_provider_custom_services', $enabled );
}

/**
 * Can providers override catalog services (hybrid only).
 */
function bookaro_allow_provider_override_catalog(): bool {
    $model = bookaro_get_services_model();
    if ( 'provider_only' === $model ) {
        return true;
    }
    if ( 'hybrid' !== $model ) {
        return false;
    }
    $enabled = function_exists( 'bookaro_get_option' )
        ? (int) bookaro_get_option( 'allow_provider_override_catalog', 1 )
        : 1;
    return (bool) apply_filters( 'bookaro_allow_provider_override_catalog', $enabled );
}

/**
 * Are provider-managed services allowed in the frontend.
 */
function bookaro_provider_services_allowed(): bool {
    $allowed = false;
    $mode = function_exists( 'bookaro_get_integration_mode' )
        ? bookaro_get_integration_mode()
        : 'auto';
    if ( 'generic' === $mode && bookaro_frontend_services_enabled() ) {
        $model = function_exists( 'bookaro_get_services_model' ) ? bookaro_get_services_model() : '';
        $allowed = in_array( $model, [ 'provider_only', 'hybrid' ], true );
    }

    return (bool) apply_filters( 'bookaro_provider_services_allowed', $allowed, $mode );
}

/**
 * Get generic services mode.
 */
function bookaro_get_generic_services_mode(): string {
    $model = bookaro_get_services_model();
    return ( 'admin_only' === $model ) ? 'admin_catalog' : $model;
}

/**
 * Normalize a service label for deduplication.
 */
function bookaro_normalize_service_label( string $label ): string {
    $label = wp_strip_all_tags( $label );
    $label = html_entity_decode( $label, ENT_QUOTES, 'UTF-8' );
    $label = preg_replace( '/\s+/', ' ', $label );
    return strtolower( trim( (string) $label ) );
}

/**
 * Deduplicate service payloads by normalized label.
 */
function bookaro_dedupe_services_by_label( array $services ): array {
    $seen = [];
    $deduped = [];

    foreach ( $services as $service ) {
        if ( ! is_array( $service ) ) {
            continue;
        }
        $label = (string) ( $service['title'] ?? '' );
        $key = bookaro_normalize_service_label( $label );
        if ( $key !== '' ) {
            if ( isset( $seen[ $key ] ) ) {
                continue;
            }
            $seen[ $key ] = true;
        }
        $deduped[] = $service;
    }

    return $deduped;
}

/**
 * Get provider-owned services for provider-first frontend/admin contexts.
 *
 * Single Business allows provider-owned services without listing context, so
 * this helper keeps those services visible in provider-first flows.
 */
function bookaro_get_provider_services_for_context( int $provider_id ): array {
    $provider_id = absint( $provider_id );
    if ( $provider_id < 1 ) {
        return [];
    }

    $services = function_exists( 'bookaro_get_bookaro_provider_services_for_provider' )
        ? bookaro_get_bookaro_provider_services_for_provider( $provider_id )
        : [];
    $services = is_array( $services ) ? $services : [];

    if ( ! function_exists( 'bookaro_is_single_business_site' ) || ! bookaro_is_single_business_site() ) {
        return $services;
    }

    $query = new \WP_Query( [
        'post_type'              => BOOKARO_SERVICE_POST_TYPE,
        'post_status'            => 'publish',
        'posts_per_page'         => -1,
        'orderby'                => [
            'menu_order' => 'ASC',
            'title'      => 'ASC',
            'ID'         => 'ASC',
        ],
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        'meta_query'             => [
            [
                'key'     => BOOKARO_SERVICE_PROVIDER_META,
                'value'   => (string) $provider_id,
                'compare' => '=',
                'type'    => 'NUMERIC',
            ],
            [
                'key'     => BOOKARO_SERVICE_OWNER_META,
                'value'   => 'provider',
                'compare' => '=',
            ],
        ],
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
    ] );

    $extra = [];
    foreach ( $query->posts as $post ) {
        if ( ! $post instanceof \WP_Post ) {
            continue;
        }
        $listing_id = absint( get_post_meta( $post->ID, BOOKARO_SERVICE_LISTING_META, true ) );
        if ( $listing_id > 0 ) {
            if ( ! bookaro_is_allowed_listing( $listing_id ) ) {
                continue;
            }
            if ( function_exists( 'bookaro_is_lite_listing_allowed' ) && ! bookaro_is_lite_listing_allowed( $listing_id ) ) {
                continue;
            }
        }
        $extra[] = bookaro_build_service_payload( $post, $listing_id );
    }

    return bookaro_dedupe_services_by_label( array_merge( $services, $extra ) );
}

/**
 * Resolve public services for a provider-first Generic/Single Business context.
 */
function bookaro_get_generic_provider_context_services( int $provider_id ): array {
    $provider_id = absint( $provider_id );
    if ( $provider_id < 1 ) {
        return [];
    }

    $model = bookaro_get_services_model();
    $allow_custom = bookaro_allow_provider_custom_services();
    $allow_override = bookaro_allow_provider_override_catalog();
    $catalog = bookaro_get_bookaro_global_services( 0 );
    $provider_services = bookaro_get_provider_services_for_context( $provider_id );

    if ( 'admin_only' === $model || 'admin_catalog' === $model ) {
        return is_array( $catalog ) ? $catalog : [];
    }
    if ( 'provider_only' === $model ) {
        return is_array( $provider_services ) ? $provider_services : [];
    }

    $catalog = is_array( $catalog ) ? $catalog : [];
    $provider_services = is_array( $provider_services ) ? $provider_services : [];

    if ( ! $allow_custom && ! $allow_override ) {
        return $catalog;
    }

    if ( ! $allow_custom ) {
        $catalog_keys = [];
        foreach ( $catalog as $service ) {
            if ( ! is_array( $service ) ) {
                continue;
            }
            $label = (string) ( $service['title'] ?? '' );
            $key = bookaro_normalize_service_label( $label );
            if ( '' !== $key ) {
                $catalog_keys[ $key ] = true;
            }
        }
        $provider_services = array_values( array_filter(
            $provider_services,
            static function ( $service ) use ( $catalog_keys ): bool {
                if ( ! is_array( $service ) || empty( $catalog_keys ) ) {
                    return false;
                }
                $label = (string) ( $service['title'] ?? '' );
                $key = bookaro_normalize_service_label( $label );
                return '' !== $key && isset( $catalog_keys[ $key ] );
            }
        ) );
    }

    $services = $allow_override
        ? array_merge( $provider_services, $catalog )
        : array_merge( $catalog, $provider_services );

    return bookaro_dedupe_services_by_label( $services );
}

/**
 * Resolve generic services for a listing (shared admin + listing-specific).
 */
function bookaro_get_services_catalog_for_listing( int $listing_id ): array {
    if ( $listing_id < 1 || ! bookaro_is_allowed_listing( $listing_id ) ) {
        return [];
    }

    $model = bookaro_get_services_model();
    $allow_custom = bookaro_allow_provider_custom_services();
    $allow_override = bookaro_allow_provider_override_catalog();
    $provider_enabled = function_exists( 'bookaro_frontend_services_enabled' )
        ? bookaro_frontend_services_enabled()
        : true;
    $context = sprintf(
        'generic:%s:%d:%d:%d',
        $model,
        $allow_custom ? 1 : 0,
        $allow_override ? 1 : 0,
        $provider_enabled ? 1 : 0
    );
    $cache_key = bookaro_get_listing_services_cache_key( $listing_id, 'generic', $context );
    $use_cache = $cache_key !== '' && bookaro_should_cache_listing_services( $listing_id, 'generic', $context );

    if ( $use_cache ) {
        $cached = get_transient( $cache_key );
        if ( is_array( $cached ) ) {
            return $cached;
        }
    }

    $admin_listing = bookaro_get_bookaro_admin_services_for_listing( $listing_id );
    $global_shared = bookaro_get_bookaro_global_services( $listing_id );
    $provider_services = $provider_enabled
        ? bookaro_get_bookaro_provider_services_for_listing( $listing_id )
        : [];

    if ( 'admin_only' === $model ) {
        $services = array_merge( $admin_listing, $global_shared );
    } elseif ( 'provider_only' === $model ) {
        $services = $provider_services;
    } else {
        if ( ! $allow_custom && ! $allow_override ) {
            $services = array_merge( $admin_listing, $global_shared );
        } else {
            if ( ! $allow_custom ) {
                $catalog = array_merge( $admin_listing, $global_shared );
                $catalog_keys = [];
                foreach ( $catalog as $service ) {
                    if ( ! is_array( $service ) ) {
                        continue;
                    }
                    $label = (string) ( $service['title'] ?? '' );
                    $key = bookaro_normalize_service_label( $label );
                    if ( '' !== $key ) {
                        $catalog_keys[ $key ] = true;
                    }
                }
                $provider_services = array_values( array_filter(
                    $provider_services,
                    static function ( $service ) use ( $catalog_keys ): bool {
                        if ( ! is_array( $service ) ) {
                            return false;
                        }
                        $label = (string) ( $service['title'] ?? '' );
                        $key = bookaro_normalize_service_label( $label );
                        return '' !== $key && isset( $catalog_keys[ $key ] );
                    }
                ) );
            }

            if ( $allow_override ) {
                $services = array_merge( $provider_services, $admin_listing, $global_shared );
            } else {
                $services = array_merge( $admin_listing, $global_shared, $provider_services );
            }
        }
    }

    $services = bookaro_dedupe_services_by_label( $services );

    if ( $use_cache ) {
        $ttl = bookaro_get_listing_services_cache_ttl( $listing_id, 'generic', $context );
        if ( $ttl > 0 ) {
            set_transient( $cache_key, $services, $ttl );
        }
    }

    return $services;
}

/**
 * Bust listing services cache when a listing is saved.
 */
function bookaro_bust_listing_services_cache_on_save( int $post_id, \WP_Post $post, bool $update ): void {
    if ( $post_id < 1 || ! isset( $post->post_type ) ) {
        return;
    }
    if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
        return;
    }
    if ( ! in_array( $post->post_type, bookaro_get_allowed_listing_post_types(), true ) ) {
        return;
    }
    bookaro_bump_listing_services_cache_buster( $post_id );
}

/**
 * Bust services cache when a service post is saved.
 */
function bookaro_bust_services_cache_on_save( int $post_id, \WP_Post $post, bool $update ): void {
    if ( $post_id < 1 || ! isset( $post->post_type ) ) {
        return;
    }
    if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
        return;
    }
    if ( BOOKARO_SERVICE_POST_TYPE !== $post->post_type ) {
        return;
    }
    bookaro_bump_services_cache_version();
    $listing_id = absint( get_post_meta( $post_id, BOOKARO_SERVICE_LISTING_META, true ) );
    if ( $listing_id > 0 ) {
        bookaro_bump_listing_services_cache_buster( $listing_id );
    }
}

/**
 * Bust services cache when a service post is deleted or trashed.
 */
function bookaro_bust_services_cache_on_delete( int $post_id ): void {
    if ( $post_id < 1 ) {
        return;
    }
    $post_type = get_post_type( $post_id );
    if ( BOOKARO_SERVICE_POST_TYPE !== $post_type ) {
        return;
    }
    bookaro_bump_services_cache_version();
    $listing_id = absint( get_post_meta( $post_id, BOOKARO_SERVICE_LISTING_META, true ) );
    if ( $listing_id > 0 ) {
        bookaro_bump_listing_services_cache_buster( $listing_id );
    }
}

/**
 * Bust services cache when listing or service meta changes.
 */
function bookaro_bust_services_cache_on_meta_change( $meta_id, int $post_id, string $meta_key, $meta_value ): void {
    // $meta_id can be an array when deleting by meta key; we don't use it here.
    if ( $post_id < 1 ) {
        return;
    }
    if ( BOOKARO_SERVICES_CACHE_META === $meta_key ) {
        return;
    }
    $post_type = get_post_type( $post_id );
    if ( ! $post_type ) {
        return;
    }
    if ( BOOKARO_SERVICE_POST_TYPE === $post_type ) {
        bookaro_bump_services_cache_version();
        $listing_id = absint( get_post_meta( $post_id, BOOKARO_SERVICE_LISTING_META, true ) );
        if ( $listing_id > 0 ) {
            bookaro_bump_listing_services_cache_buster( $listing_id );
        }
        return;
    }
    if ( in_array( $post_type, bookaro_get_allowed_listing_post_types(), true ) ) {
        bookaro_bump_listing_services_cache_buster( $post_id );
    }
}

/**
 * Resolve service label from canonical meta with legacy fallback.
 */
function bookaro_get_service_label( \WP_Post $post ): string {
    $label = (string) get_post_meta( $post->ID, BOOKARO_SERVICE_LABEL_META, true );
    if ( '' === $label ) {
        $label = $post->post_title;
    }
    return $label;
}

/**
 * Resolve service description from canonical meta with legacy fallback.
 */
function bookaro_get_service_description( \WP_Post $post ): string {
    $description = (string) get_post_meta( $post->ID, BOOKARO_SERVICE_DESCRIPTION_META, true );
    if ( '' === $description ) {
        $description = $post->post_content;
    }
    return $description;
}

/**
 * Resolve service duration minutes from canonical meta with legacy fallback.
 */
function bookaro_get_service_duration_minutes( int $service_id ): int {
    $duration = absint( get_post_meta( $service_id, BOOKARO_SERVICE_DURATION_MINUTES_META, true ) );
    if ( $duration < 1 ) {
        $duration = absint( get_post_meta( $service_id, BOOKARO_SERVICE_DURATION_META, true ) );
    }
    return $duration;
}

/**
 * Resolve shared flag from canonical meta with legacy fallback.
 */
function bookaro_get_service_is_shared( int $service_id ): int {
    $shared = absint( get_post_meta( $service_id, BOOKARO_SERVICE_IS_SHARED_META, true ) );
    if ( $shared < 1 ) {
        $shared = absint( get_post_meta( $service_id, BOOKARO_SERVICE_SHARED_META, true ) );
    }
    return $shared;
}

/**
 * Build a service payload from a service post.
 */
function bookaro_build_service_payload( \WP_Post $post, int $listing_id ): array {
    $price = (float) get_post_meta( $post->ID, BOOKARO_SERVICE_PRICE_META, true );
    $price_formatted = function_exists( 'bookaro_format_price' )
        ? bookaro_format_price( $price )
        : '';
    $label = bookaro_get_service_label( $post );
    $description = bookaro_get_service_description( $post );
    $scope = sanitize_key( (string) get_post_meta( $post->ID, BOOKARO_SERVICE_SCOPE_META, true ) );
    $owner_type = sanitize_key( (string) get_post_meta( $post->ID, BOOKARO_SERVICE_OWNER_META, true ) );
    if ( '' === $scope ) {
        if ( 'provider' === $owner_type ) {
            $scope = 'provider';
        } elseif ( $listing_id > 0 ) {
            $scope = 'listing';
        } else {
            $scope = 'global';
        }
    }
    $shared = absint( get_post_meta( $post->ID, BOOKARO_SERVICE_IS_SHARED_META, true ) );
    if ( $shared < 1 ) {
        $shared = absint( get_post_meta( $post->ID, BOOKARO_SERVICE_SHARED_META, true ) );
    }
    if ( 'provider' === $owner_type ) {
        $shared = 0;
    }
    if ( 'global' === $scope && $shared < 1 ) {
        $shared = 1;
    }
    return [
        'id'              => $post->ID,
        'title'           => $label,
        'description'     => $description,
        'price'           => $price,
        'price_formatted' => $price_formatted,
        'duration'        => bookaro_get_service_duration_minutes( $post->ID ),
        'listing_id'      => $listing_id,
        'scope'           => $scope,
        'shared'          => $shared,
    ];
}

/**
 * Get Bookaro services linked to a listing.
 */
function bookaro_get_bookaro_services_for_listing( int $listing_id ): array {
    if ( $listing_id < 1 || ! bookaro_is_allowed_listing( $listing_id ) ) {
        return [];
    }

    $query = new \WP_Query( [
        'post_type'              => BOOKARO_SERVICE_POST_TYPE,
        'post_status'            => 'publish',
        'posts_per_page'         => -1,
        'orderby'                => [
            'menu_order' => 'ASC',
            'title'      => 'ASC',
            'ID'         => 'ASC',
        ],
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        'meta_query'             => [
            [
                'key'     => BOOKARO_SERVICE_LISTING_META,
                'value'   => (string) $listing_id,
                'compare' => '=',
                'type'    => 'NUMERIC',
            ],
        ],
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
    ] );

    return array_map(
        static function ( \WP_Post $post ) use ( $listing_id ): array {
            return bookaro_build_service_payload( $post, $listing_id );
        },
        $query->posts
    );
}

/**
 * Get admin-owned services linked to a listing.
 */
function bookaro_get_bookaro_admin_services_for_listing( int $listing_id ): array {
    if ( $listing_id < 1 || ! bookaro_is_allowed_listing( $listing_id ) ) {
        return [];
    }

    $query = new \WP_Query( [
        'post_type'              => BOOKARO_SERVICE_POST_TYPE,
        'post_status'            => 'publish',
        'posts_per_page'         => -1,
        'orderby'                => [
            'menu_order' => 'ASC',
            'title'      => 'ASC',
            'ID'         => 'ASC',
        ],
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        'meta_query'             => [
            [
                'key'     => BOOKARO_SERVICE_LISTING_META,
                'value'   => (string) $listing_id,
                'compare' => '=',
                'type'    => 'NUMERIC',
            ],
            [
                'relation' => 'OR',
                [
                    'key'     => BOOKARO_SERVICE_OWNER_META,
                    'value'   => 'admin',
                    'compare' => '=',
                ],
                [
                    'key'     => BOOKARO_SERVICE_OWNER_META,
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ],
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
    ] );

    return array_map(
        static function ( \WP_Post $post ) use ( $listing_id ): array {
            return bookaro_build_service_payload( $post, $listing_id );
        },
        $query->posts
    );
}

/**
 * Get provider-owned services linked to a listing.
 */
function bookaro_get_bookaro_provider_services_for_listing( int $listing_id ): array {
    if ( $listing_id < 1 || ! bookaro_is_allowed_listing( $listing_id ) ) {
        return [];
    }

    $query = new \WP_Query( [
        'post_type'              => BOOKARO_SERVICE_POST_TYPE,
        'post_status'            => 'publish',
        'posts_per_page'         => -1,
        'orderby'                => [
            'menu_order' => 'ASC',
            'title'      => 'ASC',
            'ID'         => 'ASC',
        ],
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        'meta_query'             => [
            [
                'key'     => BOOKARO_SERVICE_LISTING_META,
                'value'   => (string) $listing_id,
                'compare' => '=',
                'type'    => 'NUMERIC',
            ],
            [
                'key'     => BOOKARO_SERVICE_OWNER_META,
                'value'   => 'provider',
                'compare' => '=',
            ],
        ],
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
    ] );

    return array_map(
        static function ( \WP_Post $post ) use ( $listing_id ): array {
            return bookaro_build_service_payload( $post, $listing_id );
        },
        $query->posts
    );
}

/**
 * Get provider-owned services by provider ID (across listings).
 */
function bookaro_get_bookaro_provider_services_for_provider( int $provider_id ): array {
    $provider_id = absint( $provider_id );
    if ( $provider_id < 1 ) {
        return [];
    }

    $query = new \WP_Query( [
        'post_type'              => BOOKARO_SERVICE_POST_TYPE,
        'post_status'            => 'publish',
        'posts_per_page'         => -1,
        'orderby'                => [
            'menu_order' => 'ASC',
            'title'      => 'ASC',
            'ID'         => 'ASC',
        ],
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        'meta_query'             => [
            [
                'key'     => BOOKARO_SERVICE_PROVIDER_META,
                'value'   => (string) $provider_id,
                'compare' => '=',
                'type'    => 'NUMERIC',
            ],
            [
                'key'     => BOOKARO_SERVICE_OWNER_META,
                'value'   => 'provider',
                'compare' => '=',
            ],
        ],
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
    ] );

    $services = [];
    foreach ( $query->posts as $post ) {
        if ( ! $post instanceof \WP_Post ) {
            continue;
        }
        $listing_id = absint( get_post_meta( $post->ID, BOOKARO_SERVICE_LISTING_META, true ) );
        if ( $listing_id < 1 || ! bookaro_is_allowed_listing( $listing_id ) ) {
            continue;
        }
        if ( function_exists( 'bookaro_is_lite_listing_allowed' ) && ! bookaro_is_lite_listing_allowed( $listing_id ) ) {
            continue;
        }
        $services[] = bookaro_build_service_payload( $post, $listing_id );
    }

    return $services;
}

/**
 * Get global Bookaro services (admin catalog).
 */
function bookaro_get_bookaro_global_services( int $listing_id = 0 ): array {
    $shared_meta_query = [
        'relation' => 'OR',
        [
            'key'     => BOOKARO_SERVICE_IS_SHARED_META,
            'value'   => '1',
            'compare' => '=',
        ],
        [
            'key'     => BOOKARO_SERVICE_SHARED_META,
            'value'   => '1',
            'compare' => '=',
        ],
    ];

    $query = new \WP_Query( [
        'post_type'              => BOOKARO_SERVICE_POST_TYPE,
        'post_status'            => 'publish',
        'posts_per_page'         => -1,
        'orderby'                => [
            'menu_order' => 'ASC',
            'title'      => 'ASC',
            'ID'         => 'ASC',
        ],
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        'meta_query'             => [
            'relation' => 'AND',
            [
                'relation' => 'OR',
                [
                    'key'     => BOOKARO_SERVICE_OWNER_META,
                    'value'   => 'admin',
                    'compare' => '=',
                ],
                [
                    'key'     => BOOKARO_SERVICE_OWNER_META,
                    'compare' => 'NOT EXISTS',
                ],
            ],
            [
                'relation' => 'OR',
                $shared_meta_query,
                [
                    'key'     => BOOKARO_SERVICE_SCOPE_META,
                    'value'   => 'global',
                    'compare' => '=',
                ],
                [
                    'key'     => BOOKARO_SERVICE_LISTING_META,
                    'value'   => '0',
                    'compare' => '=',
                    'type'    => 'NUMERIC',
                ],
                [
                    'key'     => BOOKARO_SERVICE_LISTING_META,
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ],
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
    ] );

    return array_map(
        static function ( \WP_Post $post ): array {
            return bookaro_build_service_payload( $post, 0 );
        },
        $query->posts
    );
}

/**
 * Get public services for a listing based on integration mode.
 */
function bookaro_get_public_services_for_listing( int $listing_id ): array {
    if ( $listing_id < 1 || ! bookaro_is_allowed_listing( $listing_id ) ) {
        return [];
    }
    $mode = function_exists( 'bookaro_get_integration_mode' )
        ? bookaro_get_integration_mode()
        : 'auto';
    if ( 'generic' === $mode && function_exists( 'bookaro_get_services_catalog_for_listing' ) ) {
        return bookaro_get_services_catalog_for_listing( $listing_id );
    }
    return bookaro_get_bookaro_services_for_listing( $listing_id );
}

/**
 * Get review invite settings.
 */
function bookaro_get_review_settings(): array {
    $settings = get_option( 'bookaro_review_settings', [] );
    return is_array( $settings ) ? $settings : [];
}

/**
 * Get plugin user meta.
 *
 * @param int    $user_id User ID.
 * @param string $key     Current meta key.
 * @param mixed  $default Default value if not found.
 * @return mixed
 */
function bookaro_get_user_meta( int $user_id, string $key, $default = '' ) {
    if ( metadata_exists( 'user', $user_id, $key ) ) {
        return get_user_meta( $user_id, $key, true );
    }

    return $default;
}

/**
 * Locate a template file allowing theme overrides.
 * Falls back to the plugin's templates directory when not found.
 *
 * @param string $relative Relative path inside includes/src.
 * @return string Absolute path to the template.
 */
function bookaro_locate_template( string $relative ): string {
    $path = locate_template( $relative );
    $pro_active = function_exists( 'bookaro_is_pro_active' ) && bookaro_is_pro_active();
    if ( empty( $path ) && $pro_active && defined( 'BOOKARO_PRO_PLUGIN_DIR' ) ) {
        $candidate = BOOKARO_PRO_PLUGIN_DIR . 'includes/src/' . ltrim( $relative, '/' );
        if ( file_exists( $candidate ) ) {
            $path = $candidate;
        }
    }
    if ( empty( $path ) ) {
        $path = BOOKARO_PLUGIN_DIR . 'includes/src/' . ltrim( $relative, '/' );
    }
    return $path;
}

/**
 * Register runtime hooks that should only load when WooCommerce is active.
 */
function bookaro_register_runtime_hooks(): void {
    bookaro_register_roles();
    bookaro_sync_role_caps();
    $should_log_php_errors = is_admin()
        && (bool) bookaro_get_option( 'log_php_errors', 0 );

    if ( is_admin() ) {
        add_action( 'admin_notices', 'bookaro_generic_auto_unresolved_notice' );
        add_action( 'admin_notices', 'bookaro_generic_legacy_fallback_notice' );
    }

    add_action( 'save_post', 'bookaro_bust_listing_services_cache_on_save', 10, 3 );
    add_action( 'save_post', 'bookaro_bust_services_cache_on_save', 10, 3 );
    add_action( 'before_delete_post', 'bookaro_bust_services_cache_on_delete', 10, 1 );
    add_action( 'trashed_post', 'bookaro_bust_services_cache_on_delete', 10, 1 );
    add_action( 'untrashed_post', 'bookaro_bust_services_cache_on_delete', 10, 1 );
    add_action( 'added_post_meta', 'bookaro_bust_services_cache_on_meta_change', 10, 4 );
    add_action( 'updated_post_meta', 'bookaro_bust_services_cache_on_meta_change', 10, 4 );
    add_action( 'deleted_post_meta', 'bookaro_bust_services_cache_on_meta_change', 10, 4 );

    if ( $should_log_php_errors ) {
        register_shutdown_function( static function() {
            $error = error_get_last();
            if ( ! $error ) {
                return;
            }

            $fatal_types = [
                E_ERROR,
                E_PARSE,
                E_CORE_ERROR,
                E_COMPILE_ERROR,
                E_USER_ERROR,
            ];

            if ( ! in_array( $error['type'], $fatal_types, true ) ) {
                return;
            }

            if ( ! isset( $error['file'] ) || strpos( $error['file'], BOOKARO_PLUGIN_DIR ) !== 0 ) {
                return;
            }

            if ( class_exists( '\\Bookaro\\Utils\\Logger' ) ) {
                \Bookaro\Utils\Logger::log(
                    sprintf(
                        'Fatal PHP Error: [%s] %s in %s on line %s',
                        $error['type'] ?? 'unknown',
                        $error['message'] ?? 'unknown',
                        $error['file'] ?? 'unknown',
                        $error['line'] ?? 'unknown'
                    ),
                    [],
                    true
                );
            }
        } );
    }
}

/**
 * Register Bookaro roles with their base capabilities.
 */
function bookaro_register_roles(): void {
    if ( ! function_exists( 'add_role' ) || ! function_exists( 'wp_roles' ) ) {
        return;
    }
    $roles = wp_roles();
    if ( ! $roles ) {
        return;
    }
    if ( ! $roles->get_role( 'bookaro_provider' ) ) {
        add_role(
            'bookaro_provider',
            __( 'MyBookaro Provider', 'mybookaro' ),
            [
                'read' => true,
                BOOKARO_CAP_PROVIDER => true,
            ]
        );
    }
    if ( ! $roles->get_role( 'bookaro_customer' ) ) {
        add_role(
            'bookaro_customer',
            __( 'MyBookaro Customer', 'mybookaro' ),
            [
                'read' => true,
                BOOKARO_CAP_CUSTOMER => true,
            ]
        );
    }
}

/**
 * Ensure the selected roles have Bookaro capabilities.
 */
function bookaro_sync_role_caps( ?string $provider_role = null, ?string $owner_role = null ): void {
    if ( ! function_exists( 'wp_roles' ) ) {
        return;
    }
    $roles = wp_roles();
    if ( ! $roles ) {
        return;
    }

    if ( null === $provider_role ) {
        $provider_role = bookaro_get_option( 'provider_role', '' );
    }
    if ( null === $owner_role ) {
        $owner_role = bookaro_get_option( 'owner_role', '' );
    }

    $provider_roles = array_unique( array_filter( [ 'bookaro_provider', $provider_role ] ) );
    foreach ( $provider_roles as $role_key ) {
        $role = $roles->get_role( $role_key );
        if ( $role ) {
            $role->add_cap( BOOKARO_CAP_PROVIDER );
        }
    }

    $customer_roles = array_unique( array_filter( [ 'bookaro_customer', $owner_role ] ) );
    foreach ( $customer_roles as $role_key ) {
        $role = $roles->get_role( $role_key );
        if ( $role ) {
            $role->add_cap( BOOKARO_CAP_CUSTOMER );
        }
    }
}

/**
 * Check whether a user can access provider features.
 */
function bookaro_user_has_provider_access( int $user_id = 0 ): bool {
    $user_id = $user_id ?: get_current_user_id();
    if ( ! $user_id ) {
        return false;
    }
    if ( user_can( $user_id, 'manage_options' ) ) {
        return true;
    }
    return user_can( $user_id, BOOKARO_CAP_PROVIDER );
}

/**
 * Check whether a user can access customer features.
 */
function bookaro_user_has_customer_access( int $user_id = 0 ): bool {
    $user_id = $user_id ?: get_current_user_id();
    if ( ! $user_id ) {
        return false;
    }
    if ( user_can( $user_id, 'manage_options' ) ) {
        return true;
    }
    return user_can( $user_id, BOOKARO_CAP_CUSTOMER );
}

/**
 * Check whether a user can access any Bookaro dashboard.
 */
function bookaro_user_has_dashboard_access( int $user_id = 0 ): bool {
    return bookaro_user_has_provider_access( $user_id ) || bookaro_user_has_customer_access( $user_id );
}
