<?php
/**
 * Plugin Name: Yatoon Salon Booking & Appointments
 * Plugin URI:  https://yatoon.com/bookingdemo/
 * Description: Mobile-first salon scheduling with Square and Vagaro workflows, payments, reminders, staff tools, and customer self-service.
 * Version:     4.6.2
 * Author:      Yatoon
 * Author URI:  https://yatoon.com
 * License:     GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: yatoon-booking-system
 * Domain Path: /languages
 * Requires at least: 5.8
 * Requires PHP: 7.4
 */
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

/*
 * Yatoon Appointment Booking for Salons & Spas
 * Copyright (c) 2026 Yatoon.
 *
 * This plugin code is distributed under the GPL v2 or later. Yatoon names,
 * logos, and brand assets are trademarks or brand identifiers of Yatoon.
 * Modified versions should not imply that they are official Yatoon releases
 * or are supported by Yatoon unless authorized.
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
if ( defined( 'YATOON_BOOKING_BOOTSTRAPPED' ) ) {
    return;
}
define( 'YATOON_BOOKING_BOOTSTRAPPED', true );
// -- Constants --------------------------------------------------------------
define( 'YATOON_VERSION', '4.6.2' );
// NOTE: this was pinned at '3.3.20' while the Free package moved on to
// '4.2.0'. That mismatch silently skipped every schema migration added since
// Free/Pro 3.3.20 on any Pro site whose stored yatoon_schema_version option
// was already >= 3.3.20 (i.e. most existing installs) -- including creation
// of the yatoon_clients table, the staff time-off table, and the consent/
// policy/payment/design-upload columns on yatoon_bookings and yatoon_services.
// Restored to match Free so yatoon_run_migrations() actually runs again.
define( 'YATOON_DB_VERSION', '4.6.0' );
define( 'YATOON_PACKAGE_TIER', 'free' );
define( 'YATOON_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'YATOON_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'YATOON_PLUGIN_FILE', __FILE__ );
define( 'YATOON_ASSET_SUFFIX', ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min' );
define( 'YATOON_BRAND_NAME', 'Yatoon' );
define( 'YATOON_COPYRIGHT_OWNER', 'Yatoon' );
define( 'YATOON_OFFICIAL_SITE', 'https://yatoon.com' );
define( 'YATOON_DEMO_URL', 'https://yatoon.com/bookingdemo/' );
define( 'YATOON_OFFICIAL_DOWNLOAD_URL', 'https://yatoon.com' );
define( 'YATOON_SUPPORT_URL', 'https://yatoon.com' );
if ( !defined( 'YBS_UPGRADE_URL' ) ) {
    define( 'YBS_UPGRADE_URL', 'https://checkout.freemius.com/plugin/24668/plan/40958/' );
}
// Legacy aliases (all existing code uses SBS_* constants)
define( 'SBS_VERSION', YATOON_VERSION );
define( 'SBS_PLUGIN_DIR', YATOON_PLUGIN_DIR );
define( 'SBS_PLUGIN_URL', YATOON_PLUGIN_URL );
define( 'SBS_PLUGIN_FILE', YATOON_PLUGIN_FILE );
/**
 * Load this plugin's own translation catalogs.
 *
 * 4.6.2: the package shipped seven compiled .mo catalogs in /languages and
 * called load_plugin_textdomain() exactly zero times. WordPress 6.7+ can
 * register a bundled catalog by itself from the "Domain Path" plugin header,
 * which is why this went unnoticed in development -- but this plugin declares
 * "Requires at least: 5.8", and on 5.8 through 6.6 nothing registers
 * /languages at all, so every one of those catalogs was dead weight and the
 * admin stayed English no matter what locale the site used.
 *
 * The hook is chosen by WordPress version on purpose. From 6.7 a translation
 * loaded before the `init` action is reported through _doing_it_wrong(), so
 * on those versions this must run on `init`; on 5.8-6.6 `init` would be too
 * late for strings emitted during `plugins_loaded`, so it runs there instead.
 * Priority 0 in both cases, ahead of anything that renders a string.
 */
function yatoon_load_textdomain() {
    load_plugin_textdomain(
        'yatoon-booking-system',
        false,
        dirname( plugin_basename( YATOON_PLUGIN_FILE ) ) . '/languages'
    );
}
if ( version_compare( (string) get_bloginfo( 'version' ), '6.7', '>=' ) ) {
    add_action( 'init', 'yatoon_load_textdomain', 0 );
} else {
    add_action( 'plugins_loaded', 'yatoon_load_textdomain', 0 );
}
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-core.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-schema-verifier.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-google-identity.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-apple-identity.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-facebook-identity.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-feature-modules.php';
YATOON_Feature_Modules::bootstrap();
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-privacy.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-data-retention.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-conversion-telemetry.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-release-polish.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-seo-compatibility.php';
// Restored in 4.1.5: these shipped in Free but were missing from Pro's
// bootstrap, so Pro customers had neither the infrastructure nor the native
// Gutenberg/Elementor integration that Free already had.
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-activity-log.php';
// Funnel measurement is part of the core booking journey; the heavier salon
// operations dashboard remains profile-controlled.
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-insights.php';
if ( YATOON_Feature_Modules::enabled( 'insights' ) ) {
    require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-salon-operations.php';
}
if ( YATOON_Feature_Modules::enabled( 'insights' ) || YATOON_Feature_Modules::enabled( 'operations' ) ) {
    require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-performance.php';
}
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-job-queue.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-booking-lock.php';
// 4.6.2: unconditional on purpose -- see the header comment in this file for
// why a feature-module-gated require is exactly the mistake to avoid here.
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-client-looks.php';
if ( YATOON_Feature_Modules::enabled( 'operations' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-operations.php'; }
if ( YATOON_Feature_Modules::enabled( 'commerce' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-commerce.php'; }
if ( YATOON_Feature_Modules::enabled( 'resources' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-resources.php'; }
if ( YATOON_Feature_Modules::enabled( 'rest_api' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-rest-api.php'; }
if ( YATOON_Feature_Modules::enabled( 'staff_locations' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-staff-locations.php'; }
if ( YATOON_Feature_Modules::enabled( 'group_appointments' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-group-appointments.php'; }
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-editor-integrations.php';
function yatoon_plugin_row_meta( $links, $file ) {
    if ( plugin_basename( __FILE__ ) !== $file ) {
        return $links;
    }
    $links[] = '<a href="' . esc_url( YATOON_OFFICIAL_SITE ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Official Site', 'yatoon-booking-system' ) . '</a>';
    $links[] = '<a href="' . esc_url( YATOON_DEMO_URL ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Live Demo', 'yatoon-booking-system' ) . '</a>';
    $links[] = '<a href="' . esc_url( YATOON_SUPPORT_URL ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Official Support', 'yatoon-booking-system' ) . '</a>';
    return $links;
}

add_filter(
    'plugin_row_meta',
    'yatoon_plugin_row_meta',
    10,
    2
);
if ( !function_exists( 'yatoon_enable_rtl_stylesheets' ) ) {
    /**
     * Point every Yatoon stylesheet at its -rtl.css counterpart in RTL locales.
     *
     * Before 4.5.0 the plugin had no RTL story at all: no is_rtl() handling, no
     * -rtl.css files, and no Arabic or Hebrew catalog. A salon in Dubai or Tel
     * Aviv got a booking form with every float, margin and border-radius
     * mirrored the wrong way.
     *
     * Rather than annotate ~26 enqueue call sites by hand (and miss the next
     * one somebody adds), this walks the registered queue once and marks any
     * handle whose source lives inside this plugin. WordPress then swaps in
     * "{file}-rtl.css" itself when is_rtl() is true. 'replace' rather than
     * 'add' because rtlcss emits a complete mirrored sheet, not a patch.
     *
     * Runs at priority 100 on both front-end and admin enqueue hooks so it
     * sees everything the plugin registered at default priority.
     *
     * @since 4.5.0
     * @return void
     */
    function yatoon_enable_rtl_stylesheets() {
        if ( ! is_rtl() ) {
            return;
        }
        $styles = wp_styles();
        if ( ! $styles || empty( $styles->registered ) ) {
            return;
        }
        $plugin_url = YATOON_PLUGIN_URL;
        foreach ( $styles->registered as $handle => $style ) {
            if ( empty( $style->src ) || false === strpos( (string) $style->src, $plugin_url ) ) {
                continue;
            }
            if ( isset( $style->extra['rtl'] ) ) {
                continue;
            }
            // Only claim RTL support when the mirrored file actually shipped.
            $rel = str_replace( $plugin_url, '', (string) $style->src );
            $rel = strtok( $rel, '?' );
            $rtl = preg_replace( '/\.css$/', '-rtl.css', YATOON_PLUGIN_DIR . ltrim( $rel, '/' ) );
            if ( $rtl && file_exists( $rtl ) ) {
                wp_style_add_data( $handle, 'rtl', 'replace' );
            }
        }
    }
    add_action( 'wp_enqueue_scripts', 'yatoon_enable_rtl_stylesheets', 100 );
    add_action( 'admin_enqueue_scripts', 'yatoon_enable_rtl_stylesheets', 100 );
}
if ( !function_exists( 'yatoon_set_script_i18n' ) ) {
    /**
     * Attach the plugin's JS translation catalog to an enqueued script.
     *
     * Before 4.5.0 the plugin shipped 22 locale catalogs but never called
     * wp_set_script_translations() even once, and no script declared a
     * 'wp-i18n' dependency. Every string rendered by the booking form -- which
     * is nearly all of the customer-facing copy, because the flow is drawn in
     * JavaScript -- therefore stayed English no matter which locale the site
     * ran. The .po files only ever covered the PHP-rendered chrome around it.
     *
     * Deps are patched on the registered handle rather than being passed to
     * wp_enqueue_script() at each call site, so this works no matter where or
     * in what order the script was enqueued, and adding it twice is a no-op.
     *
     * @since 4.5.0
     * @param string $handle Registered script handle.
     * @return void
     */
    function yatoon_set_script_i18n( $handle ) {
        if ( ! function_exists( 'wp_set_script_translations' ) ) {
            return;
        }
        $scripts = wp_scripts();
        if ( isset( $scripts->registered[ $handle ] ) ) {
            $deps = $scripts->registered[ $handle ]->deps;
            if ( ! in_array( 'wp-i18n', $deps, true ) ) {
                $scripts->registered[ $handle ]->deps[] = 'wp-i18n';
            }
        }
        wp_set_script_translations( $handle, 'yatoon-booking-system', YATOON_PLUGIN_DIR . 'languages' );
    }
}
if ( !function_exists( 'yatoon_get_hex_option' ) ) {
    function yatoon_get_hex_option( $option_name, $default ) {
        $value = get_option( $option_name, $default );
        if ( function_exists( 'sanitize_hex_color' ) ) {
            $value = sanitize_hex_color( $value );
        }
        return ( $value ? $value : $default );
    }

}
if ( !function_exists( 'yatoon_get_int_option' ) ) {
    function yatoon_get_int_option(
        $option_name,
        $default,
        $min,
        $max
    ) {
        $value = absint( get_option( $option_name, $default ) );
        return max( $min, min( $max, $value ) );
    }

}
if ( !function_exists( 'yatoon_secret_placeholder' ) ) {
    function yatoon_secret_placeholder( $option_name, $empty_placeholder = '' ) {
        return ( get_option( $option_name, '' ) !== '' ? 'Saved - leave blank to keep the current value' : $empty_placeholder );
    }

}
if ( !function_exists( 'yatoon_secret_saved_note' ) ) {
    function yatoon_secret_saved_note( $option_name ) {
        return ( get_option( $option_name, '' ) !== '' ? __( 'Saved securely. Leave blank unless you want to replace it.', 'yatoon-booking-system' ) : __( 'Not saved yet.', 'yatoon-booking-system' ) );
    }

}
if ( !function_exists( 'yatoon_safe_log' ) ) {
    function yatoon_safe_log( $message ) {
        if ( !defined( 'WP_DEBUG_LOG' ) || !WP_DEBUG_LOG || get_option( 'yatoon_debug_logging', '0' ) !== '1' ) {
            return;
        }
        if ( is_array( $message ) || is_object( $message ) ) {
            $message = wp_json_encode( $message );
        }
        $message = (string) $message;
        $message = preg_replace( '/(access[_-]?token|refresh[_-]?token|auth[_-]?token|secret|authorization|password|signature|manage[_-]?token)([\\s:=\"\']+)([^\\s,;&\"\']+)/i', '$1$2[redacted]', $message );
        $message = preg_replace( '/[A-Z0-9._%+\\-]+@[A-Z0-9.\\-]+\\.[A-Z]{2,}/i', '[redacted-email]', $message );
        $message = preg_replace( '/(?<!\\d)(?:\\+?\\d[\\d .()\\-]{7,}\\d)(?!\\d)/', '[redacted-phone]', $message );
        $message = preg_replace( '/([?&](?:token|key|code|signature|email|phone)=)[^&\\s]+/i', '$1[redacted]', $message );
        error_log( '[Yatoon] ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- explicitly enabled, privacy-redacted diagnostic logging.
    }

}
if ( !function_exists( 'yatoon_apple_polish_inline_css' ) ) {
    function yatoon_apple_polish_inline_css() {
        $page_bg = yatoon_get_hex_option( 'yatoon_plugin_page_bg_color', '#f5f5f7' );
        $card_bg = yatoon_get_hex_option( 'yatoon_plugin_card_bg_color', '#ffffff' );
        $border = yatoon_get_hex_option( 'yatoon_plugin_border_color', '#e5e5ea' );
        $btn_bg = yatoon_get_hex_option( 'yatoon_primary_button_bg_color', '#008a00' );
        $btn_text = yatoon_get_hex_option( 'yatoon_primary_button_text_color', '#ffffff' );
        $text_color = yatoon_get_hex_option( 'yatoon_text_color', '#1d1d1f' );
        $btn_size = yatoon_get_int_option(
            'yatoon_primary_button_font_size',
            16,
            10,
            32
        );
        $btn_radius = yatoon_get_int_option(
            'yatoon_primary_button_radius',
            999,
            0,
            999
        );
        return sprintf(
            ':root{--yatoon-skin-page-bg:%1$s;--yatoon-skin-card-bg:%2$s;--yatoon-skin-border:%3$s;--yatoon-skin-primary-btn-bg:%4$s;--yatoon-skin-primary-btn-bg-hover:%4$s;--yatoon-skin-primary-btn-text:%5$s;--yatoon-apple-text:%6$s;--yatoon-skin-primary-btn-size:%7$dpx;--yatoon-skin-primary-btn-radius:%8$dpx;}',
            esc_attr( $page_bg ),
            esc_attr( $card_bg ),
            esc_attr( $border ),
            esc_attr( $btn_bg ),
            esc_attr( $btn_text ),
            esc_attr( $text_color ),
            $btn_size,
            $btn_radius
        );
    }

}
// -- Cloudflare Worker ------------------------------------------------------
if ( !defined( 'YATOON_WORKER_URL' ) ) {
    define( 'YATOON_WORKER_URL', 'https://yatoon-api.yatoon.workers.dev' );
}
if ( !defined( 'YATOON_WORKER_SECRET' ) ) {
    define( 'YATOON_WORKER_SECRET', ( getenv( 'YATOON_WORKER_SECRET' ) ?: '' ) );
}
// -- Freemius Integration ---------------------------------------------------
if ( ! function_exists( 'yatoon_cleanup_site_after_uninstall' ) ) {
    /**
     * Remove runtime state and, only after explicit opt-in, Yatoon business data.
     *
     * Freemius owns the WordPress uninstall callback for SDK-managed plugins,
     * so this runs from its after_uninstall event rather than uninstall.php.
     */
    function yatoon_cleanup_site_after_uninstall() {
        global $wpdb;

        $cron = _get_cron_array();
        foreach ( (array) $cron as $hooks ) {
            foreach ( array_keys( (array) $hooks ) as $hook ) {
                if ( 0 === strpos( $hook, 'yatoon_' ) || 0 === strpos( $hook, 'sbs_' ) ) {
                    wp_clear_scheduled_hook( $hook );
                }
            }
        }

        // Rate limits, locks and other transient runtime state are not user data.
        $transient_prefixes = array( '_transient_yatoon_', '_transient_timeout_yatoon_', '_site_transient_yatoon_', '_site_transient_timeout_yatoon_' );
        $transient_patterns = array_map( static function ( $prefix ) use ( $wpdb ) { return $wpdb->esc_like( $prefix ) . '%'; }, $transient_prefixes );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- uninstall must remove plugin-owned runtime state directly.
        $wpdb->query( $wpdb->prepare(
            "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s",
            $transient_patterns[0],
            $transient_patterns[1],
            $transient_patterns[2],
            $transient_patterns[3]
        ) );

        if ( '1' !== get_option( 'yatoon_delete_data_on_uninstall', '0' ) ) {
            return;
        }

        $table_pattern = $wpdb->esc_like( $wpdb->prefix . 'yatoon_' ) . '%';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- uninstall must enumerate plugin-owned tables.
        $tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_pattern ) );
        foreach ( (array) $tables as $table ) {
            if ( 0 === strpos( $table, $wpdb->prefix . 'yatoon_' ) ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared -- table name is constrained to the current site's Yatoon prefix.
                $wpdb->query( 'DROP TABLE IF EXISTS `' . esc_sql( $table ) . '`' );
            }
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared -- fixed plugin-owned prefix.
        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'yatoon\\_%'" );

        // sbs_ predates the Yatoon name and is not an exclusive namespace.
        // Delete only legacy keys this plugin actually owned instead of
        // sweeping unrelated plugins that happen to share those initials.
        $legacy_options = array(
            'sbs_business_address', 'sbs_business_phone', 'sbs_business_logo', 'sbs_business_logo_id', 'sbs_business_website',
            'sbs_business_hours', 'sbs_slot_interval', 'sbs_header_offset', 'sbs_local_tz', 'sbs_utc_tz', 'sbs_tz_string',
            'sbs_square_environment', 'sbs_square_booking_widget_id', 'sbs_text_us_enabled', 'sbs_show_availability_source_note',
            'sbs_show_success_next', 'sbs_success_next_title', 'sbs_success_deposit_text', 'sbs_success_pay_later_text', 'sbs_success_followup_text',
            'sbs_consent_enabled', 'sbs_consent_title', 'sbs_consent_text', 'sbs_consent_signature_label',
            'sbs_policy_enabled', 'sbs_policy_require_ack', 'sbs_policy_title', 'sbs_policy_text', 'sbs_policy_ack_label',
            'sbs_readmore_color', 'sbs_font_family', 'sbs_heading_font', 'sbs_heading_color', 'sbs_heading_size', 'sbs_main_title_size', 'sbs_cat_tab_size',
            'sbs_btn_font', 'sbs_button_radius', 'sbs_card_radius', 'sbs_popular_label', 'sbs_max_guests',
            'sbs_continue_btn_label', 'sbs_continue_btn_size', 'sbs_continue_btn_text_color', 'sbs_continue_btn_bg_color', 'sbs_continue_btn_font', 'sbs_continue_btn_radius',
            'sbs_addmore_btn_label', 'sbs_addmore_btn_size', 'sbs_addmore_btn_text_color', 'sbs_addmore_btn_bg_color', 'sbs_addmore_btn_font', 'sbs_addmore_btn_radius',
            'sbs_addmore2_btn_label', 'sbs_addmore2_btn_size', 'sbs_addmore2_btn_text_color', 'sbs_addmore2_btn_bg_color', 'sbs_addmore2_btn_font', 'sbs_addmore2_btn_radius',
            'sbs_addmore3_btn_label', 'sbs_addmore3_btn_size', 'sbs_addmore3_btn_text_color', 'sbs_addmore3_btn_bg_color', 'sbs_addmore3_btn_font', 'sbs_addmore3_btn_radius',
            'sbs_add_for_person_btn_label', 'sbs_add_for_person_btn_size', 'sbs_add_for_person_btn_text_color', 'sbs_add_for_person_btn_bg_color', 'sbs_add_for_person_btn_font', 'sbs_add_for_person_btn_radius',
            'sbs_add_guest_btn_label', 'sbs_add_guest_btn_size', 'sbs_add_guest_btn_text_color', 'sbs_add_guest_btn_bg_color', 'sbs_add_guest_btn_font', 'sbs_add_guest_btn_radius',
            'sbs_confirm_btn_label', 'sbs_confirm_btn_size', 'sbs_confirm_btn_text_color', 'sbs_confirm_btn_bg_color', 'sbs_confirm_btn_font', 'sbs_confirm_btn_radius',
            'sbs_confirm_single_btn_label', 'sbs_confirm_single_btn_size', 'sbs_confirm_single_btn_text_color', 'sbs_confirm_single_btn_bg_color', 'sbs_confirm_single_btn_radius'
        );
        foreach ( $legacy_options as $legacy_option ) {
            delete_option( $legacy_option );
        }
    }
}

if ( ! function_exists( 'yatoon_after_freemius_uninstall' ) ) {
    /**
     * Freemius after_uninstall callback. Supports network uninstall safely.
     */
    function yatoon_after_freemius_uninstall() {
        if ( is_multisite() ) {
            $site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
            foreach ( $site_ids as $site_id ) {
                switch_to_blog( (int) $site_id );
                yatoon_cleanup_site_after_uninstall();
                restore_current_blog();
            }
            return;
        }

        yatoon_cleanup_site_after_uninstall();
    }
}

if ( !function_exists( 'ybs_fs' ) ) {
    class YBS_Freemius_Stub {
        public function can_use_premium_code() {
            return false;
            // SDK missing -- treat as free
        }

        public function is_registered() {
            return false;
        }

        public function get_upgrade_url() {
            return YBS_UPGRADE_URL;
        }
    }

    function ybs_fs() {
        global $ybs_fs;
        if ( !isset( $ybs_fs ) ) {
            $sdk = YATOON_PLUGIN_DIR . 'vendor/freemius/start.php';
            if ( !file_exists( $sdk ) ) {
                $ybs_fs = new YBS_Freemius_Stub();
                return $ybs_fs;
            }
            add_filter( 'fs_24668_caller_main_file_and_type', function ( $caller ) {
                $caller->module_type = 'plugin';
                $caller->path = plugin_basename( __FILE__ );
                return $caller;
            } );
            require_once $sdk;
            $ybs_fs = fs_dynamic_init( array(
                'id'              => '24668',
                'slug'            => 'yatoon-booking-system',
                'type'            => 'plugin',
                'public_key'      => 'pk_9ea1930d28662bc2799978e732475',
                'is_premium'      => false,
                'has_premium_sdk' => false,
                'premium_suffix'  => 'Pro',
                'has_addons'      => false,
                'has_paid_plans'  => true,
                'menu'            => array(
                    'slug'       => 'yatoon-booking-system',
                    'first-path' => 'admin.php?page=yatoon-booking-system',
                    'support'    => false,
                    'account'    => true,
                    'contact'    => false,
                    'network'    => false,
                ),
                'is_live'         => true,
            ) );
            if ( is_object( $ybs_fs ) && method_exists( $ybs_fs, 'add_action' ) ) {
                $ybs_fs->add_action( 'after_uninstall', 'yatoon_after_freemius_uninstall' );
            }
        }
        return $ybs_fs;
    }

    // Initialize the SDK while the main plugin file is being included. During
    // uninstall WordPress includes this file after plugins_loaded has already
    // fired, so deferring initialization exclusively to that hook leaves the
    // Freemius uninstall callback class and our after_uninstall listener
    // unavailable. The cached ybs_fs() call remains safe on normal requests.
    ybs_fs();

    add_action( 'plugins_loaded', function () {
        ybs_fs();
        do_action( 'ybs_fs_loaded' );
        // Covers a site that already has WP_ENVIRONMENT_TYPE=local set at the
        // time it's browsed (works on every page load, not just activation).
        if ( function_exists( 'wp_get_environment_type' ) && 'local' === wp_get_environment_type() ) {
            try {
                if ( method_exists( ybs_fs(), 'skip_connection' ) && !ybs_fs()->is_registered() ) {
                    ybs_fs()->skip_connection();
                }
            } catch ( \Throwable $e ) {
            }
        }
    }, 1 );

    // Until the Freemius opt-in/connect decision is made, its own admin_menu
    // handler (Freemius::_prepare_admin_menu, priority 999999999) leaves the
    // plugin in "activation mode": every submenu page under
    // 'yatoon-booking-system' except the top-level Settings screen -- Dashboard,
    // Bookings, Calendar, Clients, Services, Staff, Reports, AI Assistant,
    // Add-ons, and Brand & Colors -- 403s with WordPress's generic "Sorry, you
    // are not allowed to access this page", and Settings itself just shows
    // Freemius's own connect/skip prompt instead of the real settings UI. A
    // real admin normally clears this the moment they load wp-admin after
    // clicking Activate, by seeing that prompt and choosing Skip or Connect.
    // An activation run through WP-CLI (`wp plugin activate`), which hosts,
    // staging clones, and migration scripts commonly use, never shows that
    // prompt to anyone, so the site is left stuck with no explanation
    // anywhere in the UI -- found this session by activating the plugin
    // headlessly end to end and confirming every one of those pages 403s.
    //
    // This has to run from register_activation_hook, not plugins_loaded:
    // WordPress fires 'plugins_loaded' for already-active plugins BEFORE
    // include()-ing the file of the plugin currently being activated, so a
    // plugins_loaded callback registered in this file never runs during the
    // activation request itself -- and by the next request WP_CLI is no
    // longer defined (it's only true inside the wp-cli process), so a
    // WP_CLI check placed in plugins_loaded can never observe a WP-CLI
    // activation. register_activation_hook's callback, by contrast, runs
    // synchronously in the same WP-CLI process that ran the activation, so
    // WP_CLI is still correctly set when it checks.
    register_activation_hook( __FILE__, function () {
        if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
            return;
        }
        try {
            $fs = ybs_fs();
            if ( method_exists( $fs, 'skip_connection' ) && !$fs->is_registered() ) {
                $fs->skip_connection();
            }
        } catch ( \Throwable $e ) {
        }
    } );
    add_action( 'admin_notices', function () {
        if ( file_exists( YATOON_PLUGIN_DIR . 'vendor/freemius/start.php' ) ) {
            return;
        }
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        echo '<div class="notice notice-warning is-dismissible"><p>' . 'Warning: <strong>Yatoon Booking System</strong>: Freemius SDK not found. ' . 'Run <code>php install-freemius-sdk.php</code> from the plugin folder via SSH, ' . 'or manually place the <a href="https://github.com/Freemius/wordpress-sdk" target="_blank">Freemius SDK</a> into the <code>vendor/freemius/</code> folder.' . '</p></div>';
    } );
}
// -- Helper: is Pro? --------------------------------------------------------
function ybs_is_pro() {
    try {
        return ybs_fs()->can_use_premium_code();
    } catch ( \Exception $e ) {
        return false;
    }
}

function yatoon_is_pro() {
    return ybs_is_pro();
}

// -- Load Core Classes ------------------------------------------------------
// One-time fix: reset yatoon_sync_logs if it was corrupted (stored as non-array)
add_action( 'init', function () {
    $logs = get_option( 'yatoon_sync_logs' );
    if ( $logs !== false && !is_array( $logs ) ) {
        update_option( 'yatoon_sync_logs', array() );
    }
}, 1 );
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-crypto.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-activator.php';
add_action( 'admin_init', array( 'YATOON_Activator', 'migrate_large_option_autoload' ), 5 );
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-release-safety.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-health-history.php';
if ( YATOON_Feature_Modules::enabled( 'data_tools' ) ) {
    require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-data-portability.php';
    require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-backup-migration.php';
    // 4.6.1: moving in from a salon SaaS (Fresha, Booksy, Vagaro). Kept next
    // to the other data tools, but separate from data-portability, which only
    // reads other WordPress plugins' tables.
    require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-saas-import.php';
    YATOON_SaaS_Import::init();
}
if ( YATOON_Feature_Modules::enabled( 'insights' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-reporting.php'; }
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-deactivator.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-license.php';
// 4.6.2: no-show tracking / automatic deposit. Required unconditionally and
// immediately after the licence class it calls, not behind a feature module:
// the class stays inert until a Pro site switches the feature on (see
// YATOON_No_Show::is_active()), and its admin_post save handler has to exist
// even while the feature is off, or the switch that turns it on cannot post.
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-no-show.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-business-hours.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-tax.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-invoice.php';
if ( YATOON_Feature_Modules::enabled( 'notifications' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-notifications.php'; }
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-growth-modules.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-growth-storefront.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-change-service-safety.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-manage-service-catalog.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-manage-booking.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-calendar-links.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-builder-integrations.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-service-menu.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-reviews.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-discovery.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-locations.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-staff-portal.php';
if ( YATOON_Feature_Modules::enabled( 'ai' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-ai.php'; }
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-admin.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-app-shell.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-frontend.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-request-guard.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-ajax.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-cron.php';
if ( YATOON_Feature_Modules::enabled( 'customer_portal' ) ) { require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-customer-portal.php'; }
// -- PWA Support ------------------------------------------------------------
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-pwa.php';
// -- PRO / FREE Feature Classes ---------------------------------------------
// Pro package always has premium code. Freemius validates license validity.
function yatoon_missing_feature_notice( $label ) {
    add_action( 'admin_notices', function () use ( $label ) {
        if ( current_user_can( 'manage_options' ) ) {
        echo '<div class="notice notice-error"><p>' . esc_html( sprintf( __( 'Yatoon Booking System package is incomplete: %s integration file is missing. Please reinstall the latest ZIP package.', 'yatoon-booking-system' ), $label ) ) . '</p></div>';
        }
    } );
}

function yatoon_define_safe_integration_stub( $class_name, $label, $show_missing_notice = true ) {
    if ( $show_missing_notice ) { yatoon_missing_feature_notice( $label ); }
    if ( $class_name === 'YATOON_Square_API' && !class_exists( 'YATOON_Square_API' ) ) {
        class YATOON_Square_API {
            public function __construct() {
            }

            public function __call( $name, $args ) {
                return false;
            }

            public static function __callStatic( $name, $args ) {
                return false;
            }

            public function test_connection() {
                return array(
                    'success' => false,
                'error'   => __( 'Square integration file is missing.', 'yatoon-booking-system' ),
                );
            }

            public function is_configured() {
                return false;
            }

            public static function is_enabled() {
                return false;
            }

            public function is_location_closed_on( $date, $timezone = '' ) {
                return null;
            }
        }

    } elseif ( $class_name === 'YATOON_Vagaro_API' && !class_exists( 'YATOON_Vagaro_API' ) ) {
        class YATOON_Vagaro_API {
            public function __construct() {
            }

            public function __call( $name, $args ) {
                return false;
            }

            public static function __callStatic( $name, $args ) {
                return false;
            }

            public function test_connection() {
                return array(
                    'success' => false,
                'error'   => __( 'Vagaro integration file is missing.', 'yatoon-booking-system' ),
                );
            }

            public static function is_configured() {
                return false;
            }

            public function is_enabled() {
                return false;
            }
        }

    } elseif ( $class_name === 'YATOON_Google_Calendar' && !class_exists( 'YATOON_Google_Calendar' ) ) {
        class YATOON_Google_Calendar {
            public function __construct() {
            }

            public function __call( $name, $args ) {
                return false;
            }

            public static function __callStatic( $name, $args ) {
                return false;
            }

            public static function is_enabled() {
                return false;
            }

            public static function is_configured() {
                return false;
            }

            public static function get_auth_url() {
                return '';
            }

            public static function exchange_code( $code ) {
                return false;
            }

            public static function verify_oauth_state( $state ) {
                return false;
            }

            public static function get_access_token() {
                return false;
            }

            public static function add_event( $booking, $service = null, $staff = null ) {
                return false;
            }

            public static function delete_event( $event_id ) {
                return false;
            }

            public static function sync_event( $booking ) {
                return false;
            }
        }

    } elseif ( $class_name === 'YATOON_Twilio' && !class_exists( 'YATOON_Twilio' ) ) {
        class YATOON_Twilio {
            public function __construct() {
            }

            public function __call( $name, $args ) {
                return false;
            }

            public static function __callStatic( $name, $args ) {
                return false;
            }

            public static function is_enabled() {
                return false;
            }

            public static function is_configured() {
                return false;
            }

            public static function send( $to, $message ) {
                return false;
            }

            public static function send_customer_confirmation( $b, $s, $st ) {
                return false;
            }

            public static function send_admin_alert( $b, $s, $st ) {
                return false;
            }

            public static function send_reminder( $b, $s, $st ) {
                return false;
            }

            public static function test_send( $test_to ) {
                return array(
                    'success' => false,
                    'message' => __( 'Twilio integration file is missing.', 'yatoon-booking-system' ),
                );
            }
        }

    }
}

function yatoon_require_integration_file( $relative_path, $class_name, $label ) {
    $path = YATOON_PLUGIN_DIR . $relative_path;
    if ( file_exists( $path ) ) {
        require_once $path;
        return;
    }
    yatoon_define_safe_integration_stub( $class_name, $label );
}

/** True only for Yatoon administration screens, not every wp-admin request. */
function yatoon_is_plugin_admin_request() {
    if ( ! is_admin() ) { return false; }
    $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    return ( '' !== $page && ( 0 === strpos( $page, 'yatoon' ) || 0 === strpos( $page, 'sbs' ) ) )
        || ( wp_doing_ajax() && 0 === strpos( $action, 'yatoon_' ) );
}

/** Load optional integrations only when configured, handling a callback, or rendering Yatoon settings. */
function yatoon_should_load_optional_integration( $integration ) {
    if ( yatoon_is_plugin_admin_request() ) { return true; }
    $integration = sanitize_key( $integration );
    if ( 'square_api' === $integration ) { return 'square' === sanitize_key( (string) get_option( 'yatoon_active_platform', 'local' ) ); }
    if ( 'vagaro' === $integration ) { return 'vagaro' === sanitize_key( (string) get_option( 'yatoon_active_platform', 'local' ) ); }
    if ( 'google_calendar' === $integration ) { return '1' === get_option( 'yatoon_google_cal_enabled', '0' ); }
    if ( 'twilio' === $integration ) { return '1' === get_option( 'yatoon_twilio_enabled', '0' ); }
    if ( 'whatsapp' === $integration ) { return '1' === get_option( 'yatoon_whatsapp_cloud_enabled', '0' ); }
    if ( 'woocommerce' === $integration ) { return '1' === get_option( 'yatoon_woocommerce_enabled', '0' ); }
    if ( 'webhooks' === $integration ) { return '1' === get_option( 'yatoon_webhooks_enabled', '0' ); }
    if ( 'zoom' === $integration ) { return '1' === get_option( 'yatoon_zoom_enabled', '0' ); }
    if ( 'stripe' === $integration ) {
        return ( '1' === get_option( 'yatoon_deposit_enabled', '0' ) && 'stripe' === get_option( 'yatoon_deposit_gateway', 'stripe' ) )
            || isset( $_GET['yatoon_stripe_webhook'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    }
    if ( 'paypal' === $integration ) { return '1' === get_option( 'yatoon_deposit_enabled', '0' ) && 'paypal' === get_option( 'yatoon_deposit_gateway', 'stripe' ); }
    if ( 'square_payments' === $integration ) { return '1' === get_option( 'yatoon_square_payments_enabled', '0' ); }
    if ( 'square_webhook' === $integration ) {
        return 'square' === sanitize_key( (string) get_option( 'yatoon_active_platform', 'local' ) )
            || isset( $_GET['yatoon_square_webhook'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    }
    if ( 'dynamic_pricing' === $integration ) { return '1' === get_option( 'yatoon_dynamic_pricing_enabled', '0' ); }
    if ( 'microsoft_graph' === $integration ) { return '1' === get_option( 'yatoon_ms_graph_enabled', '0' ); }
    return true;
}

function yatoon_load_feature_classes() {
    if ( class_exists( 'YATOON_Square_API' ) ) {
        return;
    }
    // Only attempt to load the Pro integration files if the Pro folder is
    // actually present in THIS install - not just because the site's
    // license state claims Pro. A site's Freemius license/trial state can
    // easily be "Pro" (e.g. left over from an earlier trial or a licensed
    // upgrade that hasn't been re-installed yet) while the currently
    // installed ZIP is the Free build, which never ships the Pro files at
    // all. Checking is_pro() alone in that situation would try to load
    // files that were never included in this package, triggering a scary
    // "package is incomplete, please reinstall" notice on a perfectly
    // normal Free install. Requiring the folder to exist too means the
    // Free package - which never contains includes/pro - always and
    // reliably falls through to the safe stubs below instead.
    if ( ybs_is_pro() && is_dir( YATOON_PLUGIN_DIR . 'includes/pro' ) ) {
        if ( yatoon_should_load_optional_integration( 'square_api' ) ) { yatoon_require_integration_file( 'includes/pro/class-yatoon-square-api.php', 'YATOON_Square_API', 'Square' ); } else { yatoon_define_safe_integration_stub( 'YATOON_Square_API', 'Square', false ); }
        if ( yatoon_should_load_optional_integration( 'vagaro' ) ) { yatoon_require_integration_file( 'includes/pro/class-yatoon-vagaro-api.php', 'YATOON_Vagaro_API', 'Vagaro' ); } else { yatoon_define_safe_integration_stub( 'YATOON_Vagaro_API', 'Vagaro', false ); }
        if ( yatoon_should_load_optional_integration( 'google_calendar' ) ) { yatoon_require_integration_file( 'includes/pro/class-yatoon-google-calendar.php', 'YATOON_Google_Calendar', 'Google Calendar' ); } else { yatoon_define_safe_integration_stub( 'YATOON_Google_Calendar', 'Google Calendar', false ); }
        if ( yatoon_should_load_optional_integration( 'twilio' ) ) { yatoon_require_integration_file( 'includes/pro/class-yatoon-twilio.php', 'YATOON_Twilio', 'Twilio' ); } else { yatoon_define_safe_integration_stub( 'YATOON_Twilio', 'Twilio', false ); }
        if ( yatoon_should_load_optional_integration( 'whatsapp' ) ) { yatoon_require_integration_file( 'includes/pro/class-yatoon-whatsapp.php', 'YATOON_WhatsApp', 'WhatsApp' ); }
        if ( yatoon_should_load_optional_integration( 'woocommerce' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-woocommerce.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-woocommerce.php';
        }
        if ( yatoon_should_load_optional_integration( 'webhooks' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-webhooks.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-webhooks.php';
        }
        if ( yatoon_should_load_optional_integration( 'zoom' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-zoom.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-zoom.php';
        }
        if ( yatoon_should_load_optional_integration( 'stripe' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-stripe.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-stripe.php';
        }
        if ( yatoon_should_load_optional_integration( 'paypal' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-paypal.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-paypal.php';
        }
        if ( yatoon_should_load_optional_integration( 'square_payments' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-square-payments.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-square-payments.php';
        }
        if ( yatoon_should_load_optional_integration( 'square_webhook' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-square-webhook.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-square-webhook.php';
        }
        if ( file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-automations.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-automations.php';
        }
        if ( file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-loyalty.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-loyalty.php';
        }
        if ( file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-broadcasts.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-broadcasts.php';
        }
        if ( yatoon_should_load_optional_integration( 'dynamic_pricing' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-dynamic-pricing.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-dynamic-pricing.php';
        }
        // 4.6.1: card on file. Loaded whenever Stripe credentials exist, not
        // only when the deposit feature is on -- keeping a card and taking a
        // deposit are independent choices, and the settings screen has to be
        // reachable before the feature is switched on.
        if ( file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-card-on-file.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-card-on-file.php';
            YATOON_Card_On_File::init();
        }
        if ( yatoon_should_load_optional_integration( 'microsoft_graph' ) && file_exists( YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-microsoft-graph.php' ) ) {
            require_once YATOON_PLUGIN_DIR . 'includes/pro/class-yatoon-microsoft-graph.php';
        }
    } else {
        // Stubs return upgrade notices and never crash.
        yatoon_require_integration_file( 'includes/free/class-yatoon-square-api-stub.php', 'YATOON_Square_API', 'Square' );
        yatoon_require_integration_file( 'includes/free/class-yatoon-vagaro-api-stub.php', 'YATOON_Vagaro_API', 'Vagaro' );
        yatoon_require_integration_file( 'includes/free/class-yatoon-google-calendar-stub.php', 'YATOON_Google_Calendar', 'Google Calendar' );
        yatoon_require_integration_file( 'includes/free/class-yatoon-twilio-stub.php', 'YATOON_Twilio', 'Twilio' );
    }
}

add_action( 'plugins_loaded', 'yatoon_load_feature_classes', 2 );
// -- Hooks ------------------------------------------------------------------
register_activation_hook( __FILE__, array('YATOON_Activator', 'activate') );
register_deactivation_hook( __FILE__, array('YATOON_Deactivator', 'deactivate') );
function yatoon_run_migrations() {
    global $wpdb;
    $legacy_repair_option     = 'yatoon_legacy_schema_repairs_443_complete';
    $installed_schema_version = (string) get_option( 'yatoon_schema_version', '0' );
    $capability_version       = (string) get_option( 'yatoon_capability_version', '0' );
    if (
        ! yatoon_should_run_schema_migration( $installed_schema_version, YATOON_DB_VERSION )
        && '1' === get_option( $legacy_repair_option, '0' )
        && YATOON_VERSION === $capability_version
    ) {
        return;
    }
    // Existing sites commonly update by replacing the plugin directory, so
    // activation hooks do not run. Keep administrator capabilities in sync
    // here as well; add_cap only writes when a capability is actually missing.
    YATOON_Activator::ensure_default_capabilities();
    update_option( 'yatoon_capability_version', YATOON_VERSION, true );
    // These three repairs pre-date the versioned migration and must remain
    // independently recoverable when an older package stored a schema version
    // too early. Once every required column has been confirmed, however, do
    // not repeat SHOW TABLES/SHOW COLUMNS on every frontend, AJAX or cron hit.
    if ( '1' !== get_option( $legacy_repair_option, '0' ) ) {
        $legacy_repairs_complete = true;

        // Repair the service upload column even if an older package stored an
        // unexpected schema-version value and skipped the normal migration path.
        $design_service_table = $wpdb->prefix . 'yatoon_services';
        $design_service_table_exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $design_service_table ) );
        if ( $design_service_table_exists ) {
            $design_upload_column_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$design_service_table}` LIKE %s", 'allow_design_upload' ) );
            if ( !$design_upload_column_exists ) {
                $wpdb->query( "ALTER TABLE `{$design_service_table}`\n                 ADD COLUMN allow_design_upload tinyint(1) DEFAULT 0 AFTER allow_staff_select" );
                $design_upload_column_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$design_service_table}` LIKE %s", 'allow_design_upload' ) );
            }
        } else {
            $design_upload_column_exists = false;
        }
        $legacy_repairs_complete = $legacy_repairs_complete && $design_upload_column_exists;
    // Repair the missing design_upload_token column on the bookings table.
    // This column was added to the booking-insert code path but the matching
    // schema migration was missed, causing every new booking submitted
    // through the booking form to fail silently at the database insert step.
    // Checked unconditionally (not gated by schema_version) for the same
    // reason as the allow_design_upload repair above: an install that
    // already recorded a newer schema_version would otherwise never pick
    // this up.
        $bookings_table = $wpdb->prefix . 'yatoon_bookings';
        $bookings_table_exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $bookings_table ) );
        if ( $bookings_table_exists ) {
            $design_token_column_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$bookings_table}` LIKE %s", 'design_upload_token' ) );
            if ( !$design_token_column_exists ) {
                $wpdb->query( "ALTER TABLE `{$bookings_table}`\n                 ADD COLUMN design_upload_token varchar(64) DEFAULT NULL AFTER design_image_url" );
                $design_token_column_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$bookings_table}` LIKE %s", 'design_upload_token' ) );
            }
        } else {
            $design_token_column_exists = false;
        }
        $legacy_repairs_complete = $legacy_repairs_complete && $design_token_column_exists;
    // Repair the missing square_version column on the service options table.
    // Like the two repairs above, this column existed in the "create table"
    // SQL for brand-new installs but the matching ALTER TABLE migration for
    // sites that were already running was missing, so any query that
    // selected it (e.g. the "reuse an existing option" picker in Edit
    // Service, or saving an option copied from another service) failed with
    // a silent "Unknown column" SQL error on those older sites - wp_send_json
    // then just returned an empty result with no visible error, which looked
    // like every service simply had no options to reuse.
        $service_options_table = $wpdb->prefix . 'yatoon_service_options';
        $service_options_table_exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $service_options_table ) );
        if ( $service_options_table_exists ) {
            $square_version_column_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$service_options_table}` LIKE %s", 'square_version' ) );
            if ( !$square_version_column_exists ) {
                $wpdb->query( "ALTER TABLE `{$service_options_table}` ADD COLUMN square_version bigint(20) DEFAULT NULL" );
                $square_version_column_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$service_options_table}` LIKE %s", 'square_version' ) );
            }
        } else {
            $square_version_column_exists = false;
        }
        $legacy_repairs_complete = $legacy_repairs_complete && $square_version_column_exists;

        if ( $legacy_repairs_complete ) {
            update_option( $legacy_repair_option, '1', false );
        }
    }
    if ( !yatoon_should_run_schema_migration( $installed_schema_version, YATOON_DB_VERSION ) ) {
        return;
    }
    $schema_retry_transient = 'yatoon_main_schema_migration_retry_443';
    if ( get_transient( $schema_retry_transient ) ) {
        return;
    }
    $table_exists = static function ( $table_name ) use ( $wpdb ) {
        $table_name = sanitize_key( $table_name );
        return (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->prefix . $table_name ) );
    };
    $column_exists = static function ( $table_name, $column_name ) use ( $wpdb, $table_exists ) {
        $table_name = sanitize_key( $table_name );
        $column_name = sanitize_key( $column_name );
        if ( !$table_exists( $table_name ) ) {
            return false;
        }
        return (bool) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$wpdb->prefix}{$table_name}` LIKE %s", $column_name ) );
    };
    // -- v2.96: Rename sbs_ tables -- yatoon_ tables -----------------------
    // Only run once -- check if old tables still exist
    $tables_to_rename = array(
        'sbs_bookings'         => 'yatoon_bookings',
        'sbs_services'         => 'yatoon_services',
        'sbs_staff'            => 'yatoon_staff',
        'sbs_staff_services'   => 'yatoon_staff_services',
        'sbs_service_options'  => 'yatoon_service_options',
        'sbs_locations'        => 'yatoon_locations',
        'sbs_waitlist'         => 'yatoon_waitlist',
        'sbs_booking_options'  => 'yatoon_booking_options',
        'sbs_contact_messages' => 'yatoon_contact_messages',
    );
    foreach ( $tables_to_rename as $old_table => $new_table ) {
        $old_table = sanitize_key( $old_table );
        $new_table = sanitize_key( $new_table );
        $old_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->prefix . $old_table ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $new_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->prefix . $new_table ) );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $old_exists && !$new_exists ) {
            $wpdb->query( "RENAME TABLE `{$wpdb->prefix}{$old_table}` TO `{$wpdb->prefix}{$new_table}`" );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    // -- v2.96: Migrate sbs_ WP options -- yatoon_ options ----------------
    // Only if migration hasn't run yet
    if ( !get_option( 'yatoon_v296_options_migrated' ) ) {
        $all_options = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            "SELECT option_name, option_value FROM {$wpdb->prefix}options WHERE option_name LIKE 'sbs_%'"
         );
        foreach ( $all_options as $opt ) {
            $new_name = 'yatoon_' . substr( $opt->option_name, 4 );
            // strip 'sbs_'
            // Only migrate if yatoon_ version doesn't already exist
            if ( get_option( $new_name ) === false ) {
                add_option( $new_name, $opt->option_value );
            }
        }
        update_option( 'yatoon_v296_options_migrated', '1' );
    }
    // -- Column migrations -------------------------------------------------
    $col = $column_exists( 'yatoon_service_options', 'group_name' );
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    if ( $table_exists( 'yatoon_service_options' ) && !$col ) {
        $wpdb->query( "ALTER TABLE {$wpdb->prefix}yatoon_service_options ADD COLUMN group_name varchar(100) DEFAULT NULL AFTER is_required" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    }
    // v3.0.2: optional consent / waiver signature fields.
    $consent_columns = array();
    foreach ( yatoon_booking_required_column_definitions() as $column => $definition ) {
        $consent_columns[ $column ] = "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN `{$column}` {$definition}";
    }
    foreach ( $consent_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_bookings', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $table_exists( 'yatoon_bookings' ) && !$exists ) {
            $wpdb->query( $sql );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    $service_columns = array(
        'require_policy'      => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN require_policy tinyint(1) DEFAULT 0 AFTER is_active",
        'require_consent'     => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN require_consent tinyint(1) DEFAULT 0 AFTER require_policy",
        'allow_staff_select'  => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN allow_staff_select tinyint(1) DEFAULT 1 AFTER require_consent",
        'allow_design_upload' => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN allow_design_upload tinyint(1) DEFAULT 0 AFTER allow_staff_select",
        'buffer_before_min'   => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN buffer_before_min int(11) DEFAULT 0 AFTER allow_design_upload",
        'buffer_after_min'    => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN buffer_after_min int(11) DEFAULT 0 AFTER buffer_before_min",
        'min_lead_time_min'   => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN min_lead_time_min int(11) DEFAULT 0 AFTER buffer_after_min",
		'image_url'           => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN image_url varchar(500) DEFAULT '' AFTER min_lead_time_min",
		'estimated_cost'      => "ALTER TABLE {$wpdb->prefix}yatoon_services ADD COLUMN estimated_cost decimal(10,2) NOT NULL DEFAULT 0 AFTER price",
    );
    foreach ( $service_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_services', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $table_exists( 'yatoon_services' ) && !$exists ) {
            $wpdb->query( $sql );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    if ( $table_exists( 'yatoon_services' ) && !get_option( 'yatoon_design_upload_services_initialized' ) ) {
        // Enable the new per-service setting for existing custom-design services.
        $wpdb->query( "UPDATE {$wpdb->prefix}yatoon_services\n             SET allow_design_upload = 1\n             WHERE LOWER(name) LIKE '%custom design%'\n                OR LOWER(name) LIKE '%nail art%'" );
        update_option( 'yatoon_design_upload_services_initialized', '1' );
    }
    $staff_columns = array(
        'calendar_color' => "ALTER TABLE {$wpdb->prefix}yatoon_staff ADD COLUMN calendar_color varchar(20) DEFAULT '' AFTER image_url",
        'portal_role'    => "ALTER TABLE {$wpdb->prefix}yatoon_staff ADD COLUMN portal_role varchar(20) DEFAULT 'staff' AFTER sort_order",
    );
    foreach ( $staff_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_staff', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $table_exists( 'yatoon_staff' ) && !$exists ) {
            $wpdb->query( $sql );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    if ( $table_exists( 'yatoon_staff' ) && !get_option( 'yatoon_staff_roles_360_initialized' ) ) {
        $staff_rows = $wpdb->get_results( "SELECT id, portal_permissions FROM {$wpdb->prefix}yatoon_staff" );
        foreach ( $staff_rows as $staff_row ) {
            $permissions = ( function_exists( 'yatoon_normalize_staff_portal_permissions' ) ? explode( ',', yatoon_normalize_staff_portal_permissions( $staff_row->portal_permissions ) ) : array_filter( array_map( 'trim', explode( ',', (string) $staff_row->portal_permissions ) ) ) );
            $role = ( in_array( 'view_all_schedules', $permissions, true ) || in_array( 'manage_clients', $permissions, true ) ? 'manager' : 'staff' );
            $permissions[] = 'request_time_off';
            if ( $role === 'manager' ) {
                $permissions[] = 'approve_time_off';
            }
            $wpdb->update(
                $wpdb->prefix . 'yatoon_staff',
                array(
                    'portal_role'        => $role,
                    'portal_permissions' => implode( ',', array_values( array_unique( $permissions ) ) ),
                ),
                array(
                    'id' => (int) $staff_row->id,
                ),
                array('%s', '%s'),
                array('%d')
            );
        }
        update_option( 'yatoon_staff_roles_360_initialized', '1' );
    }
    $charset_collate = $wpdb->get_charset_collate();
    $time_off_sql = "CREATE TABLE {$wpdb->prefix}yatoon_staff_time_off_requests (\n        id bigint(20) NOT NULL AUTO_INCREMENT,\n        staff_id bigint(20) NOT NULL,\n        start_at datetime NOT NULL,\n        end_at datetime NOT NULL,\n        is_all_day tinyint(1) DEFAULT 0,\n        reason varchar(255) DEFAULT '',\n        status varchar(20) DEFAULT 'pending',\n        reviewed_by bigint(20) DEFAULT NULL,\n        review_note varchar(255) DEFAULT '',\n        created_at datetime DEFAULT CURRENT_TIMESTAMP,\n        reviewed_at datetime DEFAULT NULL,\n        PRIMARY KEY (id),\n        KEY staff_id (staff_id),\n        KEY status (status),\n        KEY start_at (start_at)\n    ) {$charset_collate};";
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $time_off_sql );
    $clients_exists = $table_exists( 'yatoon_clients' );
    if ( !$clients_exists ) {
        $charset_collate = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$wpdb->prefix}yatoon_clients (\n            id bigint(20) NOT NULL AUTO_INCREMENT,\n            customer_name varchar(255) NOT NULL DEFAULT '',\n            customer_email varchar(255) NOT NULL DEFAULT '',\n            customer_phone varchar(50) NOT NULL DEFAULT '',\n            notes text DEFAULT NULL,\n            allergies text DEFAULT NULL,\n            preferences text DEFAULT NULL,\n            preferred_staff varchar(255) DEFAULT NULL,\n            tags varchar(500) DEFAULT NULL,\n            created_at datetime DEFAULT CURRENT_TIMESTAMP,\n            updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n            PRIMARY KEY (id),\n            UNIQUE KEY customer_phone (customer_phone),\n            KEY customer_email (customer_email)\n        ) {$charset_collate};";
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
        $clients_exists = $table_exists( 'yatoon_clients' );
    }
    $client_columns = array(
        'preferences' => "ALTER TABLE {$wpdb->prefix}yatoon_clients ADD COLUMN preferences text DEFAULT NULL AFTER allergies",
    );
    foreach ( $client_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_clients', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $clients_exists && !$exists ) {
            $wpdb->query( $sql );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    $waitlist_columns = array(
        'notified_at'      => "ALTER TABLE {$wpdb->prefix}yatoon_waitlist ADD COLUMN notified_at datetime DEFAULT NULL AFTER notified",
        'matched_date'     => "ALTER TABLE {$wpdb->prefix}yatoon_waitlist ADD COLUMN matched_date date DEFAULT NULL AFTER notified_at",
        'matched_time'     => "ALTER TABLE {$wpdb->prefix}yatoon_waitlist ADD COLUMN matched_time varchar(50) DEFAULT NULL AFTER matched_date",
        'matched_start_at' => "ALTER TABLE {$wpdb->prefix}yatoon_waitlist ADD COLUMN matched_start_at varchar(100) DEFAULT NULL AFTER matched_time",
    );
    foreach ( $waitlist_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_waitlist', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $table_exists( 'yatoon_waitlist' ) && !$exists ) {
            $wpdb->query( $sql );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    add_option( 'yatoon_waitlist_auto_notify', '1' );

    // A schema version is a completion marker, never merely an attempt
    // marker. Re-probe every table/column this migration owns after all DDL
    // has run so a permissions, disk or lock failure remains retryable on the
    // next request instead of permanently closing the version gate.
    $schema_migration_complete = yatoon_main_schema_migration_complete( $table_exists, $column_exists );
    foreach ( $tables_to_rename as $old_table => $new_table ) {
        if ( $table_exists( $old_table ) && !$table_exists( $new_table ) ) {
            $schema_migration_complete = false;
            break;
        }
    }
    if ( $schema_migration_complete ) {
        update_option( 'yatoon_schema_version', YATOON_DB_VERSION );
        delete_transient( $schema_retry_transient );
    } elseif ( function_exists( 'yatoon_safe_log' ) ) {
        set_transient( $schema_retry_transient, '1', 5 * MINUTE_IN_SECONDS );
        yatoon_safe_log( 'Yatoon ' . YATOON_DB_VERSION . ': main schema migration remains incomplete and will be retried.' );
    } else {
        set_transient( $schema_retry_transient, '1', 5 * MINUTE_IN_SECONDS );
    }
}

add_action( 'plugins_loaded', 'yatoon_run_migrations', 5 );
function yatoon_init() {
    YATOON_Feature_Modules::init();
    // 4.6.2: no-show tracking. Runs here rather than at include time because
    // is_active() asks the licence, and ybs_is_pro() is only defined further
    // down this file -- by plugins_loaded it always is.
    YATOON_No_Show::init();
    YATOON_Privacy::init();
    YATOON_Data_Retention::init();
    // Restored in 4.1.5 to match Free -- see the require_once block above.
    if ( class_exists( 'YATOON_Insights' ) ) {
        YATOON_Insights::init();
    }
    // 4.6.0.1 fix: class-yatoon-insights.php is required unconditionally while
    // class-yatoon-salon-operations.php is only required when the "insights"
    // module is enabled. Gating this call on class_exists( 'YATOON_Insights' )
    // was therefore always true, so every install whose runtime profile leaves
    // insights off -- which includes the "local" and "square_single" defaults
    // chosen for a fresh install -- fataled on plugins_loaded with
    // "Class YATOON_Salon_Operations not found" and showed the WordPress
    // critical-error page on both the front end and wp-admin.
    if ( class_exists( 'YATOON_Salon_Operations' ) ) {
        YATOON_Salon_Operations::init();
    }
    if ( class_exists( 'YATOON_Performance' ) ) {
        YATOON_Performance::init();
    }
    YATOON_Job_Queue::init();
    YATOON_Client_Looks::init();
    YATOON_Google_Identity::init();
    YATOON_Apple_Identity::init();
    YATOON_Facebook_Identity::init();
    if ( class_exists( 'YATOON_Operations' ) ) { YATOON_Operations::init();
    }
    if ( class_exists( 'YATOON_Commerce' ) ) { YATOON_Commerce::init();
    }
    if ( class_exists( 'YATOON_Resources' ) ) { YATOON_Resources::init();
    }
    if ( class_exists( 'YATOON_REST_API' ) ) { YATOON_REST_API::init();
    }
    if ( class_exists( 'YATOON_Staff_Locations' ) ) { YATOON_Staff_Locations::init();
    }
    if ( class_exists( 'YATOON_Group_Appointments' ) ) { YATOON_Group_Appointments::init();
    }
    YATOON_Editor_Integrations::init();
    YATOON_Builder_Integrations::init();
    if ( is_admin() ) {
        YATOON_Release_Polish::init();
        if ( class_exists( 'YATOON_Data_Portability' ) && class_exists( 'YATOON_Backup_Migration' ) ) {
            YATOON_Data_Portability::init();
            YATOON_Backup_Migration::init();
        }
        new YATOON_Admin();
        new YATOON_App_Shell();
    }
    new YATOON_Service_Menu();
    new YATOON_Reviews();
    new YATOON_Discovery();
    // Register shortcode on both frontend and admin.
    YATOON_Locations::init();
    YATOON_Staff_Portal::init();
    new YATOON_Frontend();
    new YATOON_Ajax();
    if ( class_exists( 'YATOON_AI' ) ) { new YATOON_AI();
    }
    new YATOON_Cron();
    if ( class_exists( 'YATOON_Automations' ) ) {
        new YATOON_Automations();
    }
    if ( class_exists( 'YATOON_Loyalty' ) ) {
        new YATOON_Loyalty();
    }
    if ( class_exists( 'YATOON_Broadcasts' ) ) {
        new YATOON_Broadcasts();
    }
    if ( class_exists( 'YATOON_Dynamic_Pricing' ) ) {
        YATOON_Dynamic_Pricing::init();
    }
    if ( class_exists( 'YATOON_Microsoft_Graph' ) ) {
        YATOON_Microsoft_Graph::init();
    }
    if ( class_exists( 'YATOON_Customer_Portal' ) ) { new YATOON_Customer_Portal();
    }
    if ( class_exists( 'YATOON_Notifications' ) ) { new YATOON_Notifications();
    }
    new YATOON_Calendar_Links();
    if ( class_exists( 'YATOON_PWA' ) ) {
        new YATOON_PWA();
    }
}

add_action( 'plugins_loaded', 'yatoon_init' );
