<?php

/**
 * Plugin Name: B3 Swatches
 * Plugin URI: https://b3.digital/plugins/b3-swatches/
 * Description: Replace WooCommerce variation dropdowns with modern variation swatches.
 * Version: 1.0.1
 * Author: B3 Digital
 * Author URI: https://b3.digital/
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: b3-swatches
 * Domain Path: /languages
 * Requires at least: 6.8
 * Tested up to: 6.9
 * Requires PHP: 7.4
 * Requires Plugins: woocommerce
 * WC requires at least: 6.8
 * WC tested up to: 10.6.1
 *
 *
 *
 * @package B3WVS\VariationSwatches
 */
defined( 'ABSPATH' ) || exit;
if ( function_exists( 'b3wvs_fs' ) ) {
    b3wvs_fs()->set_basename( false, __FILE__ );
} else {
    // WordPress 6.7+ pre-registers the plugin textdomain from the header metadata.
    // Freemius can hit __( '...', 'b3-swatches' ) before init runs, which would make
    // WordPress attempt JIT loading and emit the _load_textdomain_just_in_time notice.
    // Pre-filling $l10n['b3-swatches'] with NOOP_Translations short-circuits that early
    // JIT path, and the filter below suppresses the notice if WordPress still reaches it.
    // Real translations can still load later through the normal WordPress flow.
    add_filter(
        'doing_it_wrong_trigger_error',
        static function ( $trigger, $function_name, $message ) {
            if ( '_load_textdomain_just_in_time' === $function_name && is_string( $message ) && false !== strpos( $message, "'b3-swatches'" ) ) {
                return false;
            }
            return $trigger;
        },
        0,
        3
    );
    if ( !isset( $GLOBALS['l10n']['b3-swatches'] ) && class_exists( 'NOOP_Translations' ) ) {
        $GLOBALS['l10n']['b3-swatches'] = new NOOP_Translations();
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional: pre-marks domain as loaded to prevent WP 6.7+ JIT notice before init fires.
    }
    // Keep a second guard before declaring the helper in case another load path
    // defined b3wvs_fs() after the outer bootstrap check.
    if ( !function_exists( 'b3wvs_fs' ) ) {
        // Load the Composer autoloader when present (typically local/dev installs).
        if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
            require_once __DIR__ . '/vendor/autoload.php';
        }
        // Include Freemius SDK.
        // Production: build script stages the unscoped SDK at /vendor/freemius/wordpress-sdk/.
        // Development: Composer installs the SDK at the same path.
        // The SDK's own start.php handles cross-plugin version arbitration
        // (newest SDK wins), so no Strauss scoping is needed.
        if ( file_exists( __DIR__ . '/vendor/freemius/wordpress-sdk/start.php' ) ) {
            require_once __DIR__ . '/vendor/freemius/wordpress-sdk/start.php';
        } elseif ( file_exists( __DIR__ . '/freemius/start.php' ) ) {
            require_once __DIR__ . '/freemius/start.php';
        }
        /**
         * Create a helper function for easy SDK access.
         *
         * @return \Freemius
         */
        function b3wvs_fs() {
            // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Bootstrap helper for Freemius integration.
            global $b3wvs_fs;
            if ( !isset( $b3wvs_fs ) ) {
                // Include Freemius SDK.
                // SDK is auto-loaded through Composer.
                $b3wvs_fs = fs_dynamic_init( array(
                    'id'               => '23384',
                    'slug'             => 'b3-swatches',
                    'premium_slug'     => 'b3-swatches-pro',
                    'type'             => 'plugin',
                    'public_key'       => 'pk_1da36d56d705d15b49596c81eaac3',
                    'is_premium'       => false,
                    'premium_suffix'   => '(Pro)',
                    'has_addons'       => false,
                    'has_paid_plans'   => true,
                    'is_org_compliant' => true,
                    'trial'            => array(
                        'days'               => 30,
                        'is_require_payment' => true,
                    ),
                    'menu'             => array(
                        'slug'       => 'b3-swatches',
                        'first-path' => 'admin.php?page=b3-swatches',
                        'support'    => false,
                    ),
                    'is_live'          => true,
                ) );
            }
            return $b3wvs_fs;
        }

        // Init Freemius.
        b3wvs_fs();
        // Signal that SDK was initiated.
        do_action( 'b3wvs_fs_loaded' );
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Freemius bootstrap hook.
        b3wvs_fs()->add_action( 'after_uninstall', 'b3wvs_fs_uninstall_cleanup' );
    }
}
/**
 * Handle plugin uninstall cleanup.
 *
 * Hooked to Freemius after_uninstall action so uninstall feedback is tracked.
 *
 * By default this plugin keeps data on uninstall.
 * To remove data, enable the setting or return true from the filter:
 * add_filter( 'b3wvs_uninstall_remove_data', '__return_true' );
 */
function b3wvs_fs_uninstall_cleanup() {
    // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Freemius uninstall callback name kept stable.
    $settings = get_option( 'b3_wvs_settings', array() );
    if ( !is_array( $settings ) ) {
        $settings = array();
    }
    $remove_on_uninstall = !empty( $settings['remove_on_uninstall'] );
    // Developers can still override this behavior via the filter.
    $should_remove = (bool) apply_filters( 'b3wvs_uninstall_remove_data', $remove_on_uninstall );
    if ( !$should_remove ) {
        return;
    }
    // Settings.
    delete_option( 'b3_wvs_settings' );
    if ( is_multisite() ) {
        delete_site_option( 'b3_wvs_settings' );
    }
    delete_transient( 'b3_wvs_usage_stats_v1' );
    if ( is_multisite() ) {
        delete_site_transient( 'b3_wvs_usage_stats_v1' );
    }
    // Per-product overrides.
    if ( function_exists( 'delete_post_meta_by_key' ) ) {
        delete_post_meta_by_key( '_b3_wvs_overrides' );
    }
    // Term meta.
    global $wpdb;
    $meta_keys = array('b3_wvs_type', 'b3_wvs_color');
    foreach ( $meta_keys as $meta_key ) {
        delete_metadata(
            'term',
            0,
            $meta_key,
            '',
            true
        );
    }
    // Reset attribute types back to the WooCommerce default dropdown.
    $attribute_table_name = $wpdb->prefix . 'woocommerce_attribute_taxonomies';
    // Validate/escape the table identifier before interpolating into SQL.
    // Plugin Check is strict about dynamic identifiers, even when values are prepared.
    if ( preg_match( '/^[A-Za-z0-9_]+$/', $attribute_table_name ) ) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup: direct schema check is intentional.
        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $attribute_table_name ) ) );
        if ( $table_exists === $attribute_table_name ) {
            $types_to_reset = array('color', 'button');
            foreach ( $types_to_reset as $old_type ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup: direct update is intentional.
                $wpdb->update(
                    $attribute_table_name,
                    array(
                        'attribute_type' => 'select',
                    ),
                    array(
                        'attribute_type' => $old_type,
                    ),
                    array('%s'),
                    array('%s')
                );
            }
        }
    }
    wp_cache_flush();
}

/**
 * Clear cached admin usage statistics.
 *
 * @return void
 */
function b3wvs_flush_usage_stats_cache() {
    // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Shared bootstrap helper for onboarding usage cache invalidation.
    delete_transient( 'b3_wvs_usage_stats_v1' );
    if ( is_multisite() ) {
        delete_site_transient( 'b3_wvs_usage_stats_v1' );
    }
}

/**
 * Recursively delete a directory tree created by the plugin.
 *
 * @param string $path Absolute directory path.
 * @return void
 */
function b3wvs_delete_directory_tree(  $path  ) {
    // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Bootstrap-level uninstall helper.
    $path = ( is_string( $path ) ? wp_normalize_path( $path ) : '' );
    if ( '' === $path || !is_dir( $path ) ) {
        return;
    }
    $entries = glob( trailingslashit( $path ) . '*' );
    if ( !is_array( $entries ) ) {
        return;
    }
    foreach ( $entries as $entry ) {
        if ( is_dir( $entry ) ) {
            b3wvs_delete_directory_tree( $entry );
            continue;
        }
        if ( is_file( $entry ) ) {
            wp_delete_file( $entry );
        }
    }
    $remaining = glob( trailingslashit( $path ) . '*' );
    if ( is_array( $remaining ) && 0 === count( $remaining ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Safe: directory confirmed empty after recursive cleanup.
        rmdir( $path );
    }
}

// Define plugin constants.
define( 'B3_WVS_VERSION', '1.0.1' );
define( 'B3_WVS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'B3_WVS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
// Declare WooCommerce HPOS compatibility.
add_action( 'before_woocommerce_init', function () {
    if ( class_exists( '\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil' ) ) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
    }
} );
// Simple PSR-4 autoloader for our plugin classes.
spl_autoload_register( function ( $class_name ) {
    $prefix = 'B3WVS\\VariationSwatches\\';
    $base_dir = __DIR__ . '/includes/';
    $len = strlen( $prefix );
    if ( strncmp( $prefix, $class_name, $len ) !== 0 ) {
        return;
    }
    $relative_class = substr( $class_name, $len );
    $file = $base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
    if ( file_exists( $file ) ) {
        require $file;
    }
} );
/**
 * Return settings with any active settings restrictions applied when available.
 *
 * @param array|null $settings Optional settings payload. Reads from the database when omitted.
 * @return array
 */
function b3wvs_get_effective_settings(  $settings = null  ) {
    if ( null === $settings ) {
        $settings = get_option( 'b3_wvs_settings', array() );
    }
    if ( !is_array( $settings ) ) {
        $settings = array();
    }
    return array_merge( \B3WVS\VariationSwatches\Admin\Settings\Defaults::get(), $settings );
}

/**
 * Main plugin class.
 */
final class B3_Variation_Swatches {
    /**
     * Plugin version.
     */
    const VERSION = '1.0.1';

    /**
     * Instance of this class.
     *
     * @var B3_Variation_Swatches
     */
    private static $instance = null;

    /**
     * Get instance of this class.
     *
     * @return B3_Variation_Swatches
     */
    public static function get_instance() {
        if ( is_null( self::$instance ) ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor.
     */
    private function __construct() {
        $this->init();
    }

    /**
     * Initialize the plugin.
     */
    private function init() {
        add_action( 'plugins_loaded', array($this, 'check_dependencies') );
    }

    /**
     * Check if WooCommerce is active and other dependencies.
     */
    public function check_dependencies() {
        if ( !class_exists( 'WooCommerce' ) ) {
            add_action( 'admin_notices', array($this, 'woocommerce_missing_notice') );
            return;
        }
        $this->includes();
        $this->hooks();
    }

    /**
     * Show notice if WooCommerce is missing.
     */
    public function woocommerce_missing_notice() {
        ?>
		<div class="error">
			<p><?php 
        esc_html_e( 'B3 Swatches requires WooCommerce to be installed and active.', 'b3-swatches' );
        ?></p>
		</div>
			<?php 
    }

    /**
     * Include required files.
     */
    private function includes() {
        // Core classes autoload through the bootstrap autoloader above.
        $this->boot_component( 'assets', static function () {
            \B3WVS\VariationSwatches\Assets::get_instance();
        } );
        if ( !is_admin() ) {
            $this->boot_component( 'admin_bar', static function () {
                \B3WVS\VariationSwatches\Admin\AdminBar::get_instance();
            } );
        }
        if ( $this->should_boot_settings_controller() ) {
            $this->boot_component( 'settings', static function () {
                \B3WVS\VariationSwatches\Admin\Settings::get_instance();
            } );
        }
        if ( is_admin() ) {
            $this->boot_component( 'product_swatches', static function () {
                \B3WVS\VariationSwatches\Admin\ProductSwatches::get_instance();
            } );
            $this->boot_component( 'term_meta', static function () {
                \B3WVS\VariationSwatches\Admin\TermMeta::get_instance();
            } );
        }
        $this->boot_component( 'frontend_swatches', static function () {
            \B3WVS\VariationSwatches\Frontend\Swatches::get_instance();
        } );
    }

    /**
     * Determine whether the shared settings controller should boot.
     *
     * Normal frontend product requests should not depend on admin controller
     * initialization, but the admin app's REST requests still need the
     * settings and font routes even before WordPress defines REST_REQUEST.
     *
     * @return bool
     */
    private function should_boot_settings_controller() {
        if ( is_admin() ) {
            return true;
        }
        return $this->is_rest_request();
    }

    /**
     * Detect REST requests early enough for plugins_loaded bootstrap.
     *
     * WordPress defines REST_REQUEST after plugins_loaded, so relying only on
     * that constant causes route registration to be skipped on the actual
     * /wp-json requests that the admin app makes.
     *
     * @return bool
     */
    private function is_rest_request() {
        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return true;
        }
        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            return false;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only request inspection for early REST route detection.
        if ( isset( $_GET['rest_route'] ) ) {
            return true;
        }
        if ( !isset( $_SERVER['REQUEST_URI'] ) ) {
            return false;
        }
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately for path-only comparison.
        $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
        $request_path = wp_parse_url( $request_uri, PHP_URL_PATH );
        $rest_prefix = ( function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json' );
        if ( !is_string( $request_path ) || !is_string( $rest_prefix ) || '' === $rest_prefix ) {
            return false;
        }
        $request_path = trailingslashit( '/' . ltrim( $request_path, '/' ) );
        $rest_prefix = trim( $rest_prefix, '/' );
        if ( false !== strpos( $request_path, '/' . $rest_prefix . '/' ) ) {
            return true;
        }
        return false !== strpos( $request_path, '/index.php/' . $rest_prefix . '/' );
    }

    /**
     * Boot one component without letting failures disable unrelated plugin areas.
     *
     * @param string   $component Component label for debug logging.
     * @param callable $callback  Component bootstrap callback.
     * @return void
     */
    private function boot_component( $component, $callback ) {
        try {
            $callback();
        } catch ( \Throwable $e ) {
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only log in WP_DEBUG mode.
                error_log( 'B3 Swatches initialization error (' . $component . '): ' . $e->getMessage() );
            }
        }
    }

    /**
     * Register hooks.
     */
    private function hooks() {
    }

}

/**
 * Returns the main instance of B3_Variation_Swatches.
 *
 * @return B3_Variation_Swatches
 */
function b3wvs_get_instance() {
    // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed -- Bootstrap file contains both singleton class and helper functions.
    return B3_Variation_Swatches::get_instance();
}

// Start the plugin.
b3wvs_get_instance();