<?php
/**
 * Plugin Name: Yatoon Salon Booking - Appointment Calendar for Salons & Spas
 * Plugin URI:  https://yatoon.com/bookingdemo/
 * Description: Mobile-first appointment booking for salons, spas, nail studios, and beauty businesses. Pro adds Square payments and POS sync.
 * Version:     4.3.0
 * 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.PHP.DevelopmentFunctions.error_log_error_log
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange

/*
 * 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.3.0' );
// 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.2.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 );
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-core.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-privacy.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-conversion-telemetry.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';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-job-queue.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-performance.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-operations.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-commerce.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-resources.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_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 = preg_replace( '/(access[_-]?token|refresh[_-]?token|auth[_-]?token|secret|authorization)([\\s:=]+)([^\\s,&]+)/i', '$1$2[redacted]', (string) $message );
        error_log( '[Yatoon] ' . $message );
    }

}
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( '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,
                '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,
            ) );
        }
        return $ybs_fs;
    }

    add_action( 'plugins_loaded', function () {
        ybs_fs();
        do_action( 'ybs_fs_loaded' );
        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 );
    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';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-deactivator.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-license.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-business-hours.php';
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-manage-booking.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-calendar-links.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-service-menu.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-locations.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-staff-portal.php';
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-ajax.php';
require_once YATOON_PLUGIN_DIR . 'includes/class-yatoon-cron.php';
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.', $label ) ) . '</p></div>';
        }
    } );
}

function yatoon_define_safe_integration_stub(  $class_name, $label  ) {
    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.',
                );
            }

            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.',
                );
            }

            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.',
                );
            }

        }

    }
}

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 );
}

function yatoon_load_feature_classes() {
    if ( class_exists( 'YATOON_Square_API' ) ) {
        return;
    }

    // Free package 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;
    // 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" );
        }
    }
    $installed_schema_version = (string) get_option( 'yatoon_schema_version', '0' );
    // 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" );
        }
    }
    if ( !yatoon_should_run_schema_migration( $installed_schema_version, YATOON_DB_VERSION ) ) {
        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 ) {
            // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter
            $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 ) {
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $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(
        'consent_accepted'       => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN consent_accepted tinyint(1) DEFAULT 0 AFTER google_event_id",
        'consent_signature'      => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN consent_signature varchar(255) DEFAULT NULL AFTER consent_accepted",
        'consent_signed_at'      => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN consent_signed_at datetime DEFAULT NULL AFTER consent_signature",
        'consent_text_snapshot'  => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN consent_text_snapshot text DEFAULT NULL AFTER consent_signed_at",
        'policy_acknowledged'    => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN policy_acknowledged tinyint(1) DEFAULT 0 AFTER consent_text_snapshot",
        'policy_acknowledged_at' => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN policy_acknowledged_at datetime DEFAULT NULL AFTER policy_acknowledged",
        'policy_text_snapshot'   => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN policy_text_snapshot text DEFAULT NULL AFTER policy_acknowledged_at",
        'payment_status'         => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN payment_status varchar(30) DEFAULT 'unpaid' AFTER policy_text_snapshot",
        'payment_note'           => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN payment_note text DEFAULT NULL AFTER payment_status",
        'payment_updated_at'     => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN payment_updated_at datetime DEFAULT NULL AFTER payment_note",
        'design_attachment_id'   => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN design_attachment_id bigint(20) DEFAULT NULL AFTER notes",
        'design_image_url'       => "ALTER TABLE {$wpdb->prefix}yatoon_bookings ADD COLUMN design_image_url varchar(500) DEFAULT NULL AFTER design_attachment_id",
    );
    foreach ( $consent_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_bookings', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $table_exists( 'yatoon_bookings' ) && !$exists ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $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",
    );
    foreach ( $service_columns as $column => $sql ) {
        $exists = $column_exists( 'yatoon_services', $column );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        if ( $table_exists( 'yatoon_services' ) && !$exists ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $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 ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $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 IF NOT EXISTS {$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 ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $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 ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->query( $sql );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        }
    }
    add_option( 'yatoon_waitlist_auto_notify', '1' );
    update_option( 'yatoon_schema_version', YATOON_DB_VERSION );
}

add_action( 'plugins_loaded', 'yatoon_run_migrations', 5 );
function yatoon_init() {
    YATOON_Privacy::init();
    // Restored in 4.1.5 to match Free -- see the require_once block above.
    YATOON_Job_Queue::init();
    YATOON_Performance::init();
    YATOON_Operations::init();
    YATOON_Commerce::init();
    YATOON_Resources::init();
    YATOON_Editor_Integrations::init();
    if ( is_admin() ) {
        new YATOON_Admin();
        new YATOON_App_Shell();
    }
    new YATOON_Service_Menu();
    // Register shortcode on both frontend and admin.
    YATOON_Locations::init();
    YATOON_Staff_Portal::init();
    new YATOON_Frontend();
    new YATOON_Ajax();
    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();
    }
    new YATOON_Customer_Portal();
    new YATOON_Notifications();
    new YATOON_Calendar_Links();
    if ( get_option( 'yatoon_pwa_enabled', '0' ) === '1' ) {
        new YATOON_PWA();
    }
}

add_action( 'plugins_loaded', 'yatoon_init' );


